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 | |
| parent | 3cb5cda7583566cec66aabf3543b0d876a864369 (diff) | |
media plugin
Diffstat (limited to 'plugins/org.apache.cordova.media/src')
| -rw-r--r-- | plugins/org.apache.cordova.media/src/android/AudioHandler.java | 410 | ||||
| -rw-r--r-- | plugins/org.apache.cordova.media/src/android/AudioPlayer.java | 587 | ||||
| -rw-r--r-- | plugins/org.apache.cordova.media/src/android/FileHelper.java | 38 | ||||
| -rw-r--r-- | plugins/org.apache.cordova.media/src/blackberry10/index.js | 237 | ||||
| -rw-r--r-- | plugins/org.apache.cordova.media/src/ios/CDVSound.h | 113 | ||||
| -rw-r--r-- | plugins/org.apache.cordova.media/src/ios/CDVSound.m | 703 | ||||
| -rw-r--r-- | plugins/org.apache.cordova.media/src/tizen/MediaProxy.js | 223 | ||||
| -rw-r--r-- | plugins/org.apache.cordova.media/src/ubuntu/media.cpp | 128 | ||||
| -rw-r--r-- | plugins/org.apache.cordova.media/src/ubuntu/media.h | 267 | ||||
| -rw-r--r-- | plugins/org.apache.cordova.media/src/windows8/MediaProxy.js | 217 | ||||
| -rw-r--r-- | plugins/org.apache.cordova.media/src/wp/AudioPlayer.cs | 647 | ||||
| -rw-r--r-- | plugins/org.apache.cordova.media/src/wp/Media.cs | 590 |
12 files changed, 4160 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; + } +} diff --git a/plugins/org.apache.cordova.media/src/blackberry10/index.js b/plugins/org.apache.cordova.media/src/blackberry10/index.js new file mode 100644 index 00000000..1b9b7860 --- /dev/null +++ b/plugins/org.apache.cordova.media/src/blackberry10/index.js @@ -0,0 +1,237 @@ +/* + * + * 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. + * +*/ + +var audioObjects = {}, + mediaErrorsHandled = false; + +// There is a bug in the webplatform handling of media error +// dialogs prior to 10.2. This function needs to be run once +// on the webview which plays audio to prevent freezing. +function handleMediaErrors() { + var webview = qnx.webplatform.getWebViews()[0], + handler = webview.onDialogRequested; + if (!mediaErrorsHandled) { + webview.allowWebEvent("DialogRequested"); + webview.onDialogRequested = undefined; + webview.onDialogRequested = function (eventArgs) { + var parsedArgs = JSON.parse(eventArgs); + if (parsedArgs.dialogType === 'MediaError') { + return '{"setPreventDefault": true}'; + } + handler(eventArgs); + }; + mediaErrorsHandled = true; + } +} + +module.exports = { + + create: function (success, fail, args, env) { + var result = new PluginResult(args, env), + id; + + if (!args[0]) { + result.error("Media Object id was not sent in arguments"); + return; + } + + id = JSON.parse(decodeURIComponent(args[0])); + + if (!args[1]){ + audioObjects[id] = new Audio(); + } else { + audioObjects[id] = new Audio(JSON.parse(decodeURIComponent(args[1]))); + } + + handleMediaErrors(); + + result.ok(); + }, + + startPlayingAudio: function (success, fail, args, env) { + + var audio, + id, + result = new PluginResult(args, env); + + if (!args[0]) { + result.error("Media Object id was not sent in arguments"); + return; + } + + id = JSON.parse(decodeURIComponent(args[0])); + + audio = audioObjects[id]; + + if (!audio) { + result.error("Audio object has not been initialized"); + } else { + audio.play(); + result.ok(); + } + }, + + stopPlayingAudio: function (success, fail, args, env) { + + var audio, + id, + result = new PluginResult(args, env); + + if (!args[0]) { + result.error("Media Object id was not sent in arguments"); + return; + } + + id = JSON.parse(decodeURIComponent(args[0])); + + audio = audioObjects[id]; + + if (!audio) { + result.error("Audio Object has not been initialized"); + return; + } + + audio.pause(); + audio.currentTime = 0; + + result.ok(); + }, + + seekToAudio: function (success, fail, args, env) { + + var audio, + result = new PluginResult(args, env); + + if (!args[0]) { + result.error("Media Object id was not sent in arguments"); + return; + } + + audio = audioObjects[JSON.parse(decodeURIComponent(args[0]))]; + + if (!audio) { + result.error("Audio Object has not been initialized"); + } else if (!args[1]) { + result.error("Media seek time argument not found"); + } else { + try { + audio.currentTime = JSON.parse(decodeURIComponent(args[1])) / 1000; + result.ok(); + } catch (e) { + result.error("Error seeking audio: " + e); + } + } + }, + + pausePlayingAudio: function (success, fail, args, env) { + + var audio, + result = new PluginResult(args, env); + + if (!args[0]) { + result.error("Media Object id was not sent in arguments"); + return; + } + + audio = audioObjects[JSON.parse(decodeURIComponent(args[0]))]; + + if (!audio) { + result.error("Audio Object has not been initialized"); + return; + } + + audio.pause(); + }, + + getCurrentPositionAudio: function (success, fail, args, env) { + + var audio, + result = new PluginResult(args, env); + + if (!args[0]) { + result.error("Media Object id was not sent in arguments"); + return; + } + + audio = audioObjects[JSON.parse(decodeURIComponent(args[0]))]; + + if (!audio) { + result.error("Audio Object has not been initialized"); + return; + } + + result.ok(audio.currentTime); + }, + + getDuration: function (success, fail, args, env) { + + var audio, + result = new PluginResult(args, env); + + if (!args[0]) { + result.error("Media Object id was not sent in arguments"); + return; + } + + audio = audioObjects[JSON.parse(decodeURIComponent(args[0]))]; + + if (!audio) { + result.error("Audio Object has not been initialized"); + return; + } + + result.ok(audio.duration); + }, + + startRecordingAudio: function (success, fail, args, env) { + var result = new PluginResult(args, env); + result.error("Not supported"); + }, + + stopRecordingAudio: function (success, fail, args, env) { + var result = new PluginResult(args, env); + result.error("Not supported"); + }, + + release: function (success, fail, args, env) { + var audio, + id, + result = new PluginResult(args, env); + + if (!args[0]) { + result.error("Media Object id was not sent in arguments"); + return; + } + + id = JSON.parse(decodeURIComponent(args[0])); + + audio = audioObjects[id]; + + if (audio) { + if(audio.src !== ""){ + audio.src = undefined; + } + audioObjects[id] = undefined; + } + + result.ok(); + } +}; diff --git a/plugins/org.apache.cordova.media/src/ios/CDVSound.h b/plugins/org.apache.cordova.media/src/ios/CDVSound.h new file mode 100644 index 00000000..984924de --- /dev/null +++ b/plugins/org.apache.cordova.media/src/ios/CDVSound.h @@ -0,0 +1,113 @@ +/* + 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. + */ + +#import <Foundation/Foundation.h> +#import <AudioToolbox/AudioServices.h> +#import <AVFoundation/AVFoundation.h> + +#import <Cordova/CDVPlugin.h> + +enum CDVMediaError { + MEDIA_ERR_ABORTED = 1, + MEDIA_ERR_NETWORK = 2, + MEDIA_ERR_DECODE = 3, + MEDIA_ERR_NONE_SUPPORTED = 4 +}; +typedef NSUInteger CDVMediaError; + +enum CDVMediaStates { + MEDIA_NONE = 0, + MEDIA_STARTING = 1, + MEDIA_RUNNING = 2, + MEDIA_PAUSED = 3, + MEDIA_STOPPED = 4 +}; +typedef NSUInteger CDVMediaStates; + +enum CDVMediaMsg { + MEDIA_STATE = 1, + MEDIA_DURATION = 2, + MEDIA_POSITION = 3, + MEDIA_ERROR = 9 +}; +typedef NSUInteger CDVMediaMsg; + +@interface CDVAudioPlayer : AVAudioPlayer +{ + NSString* mediaId; +} +@property (nonatomic, copy) NSString* mediaId; +@end + +@interface CDVAudioRecorder : AVAudioRecorder +{ + NSString* mediaId; +} +@property (nonatomic, copy) NSString* mediaId; +@end + +@interface CDVAudioFile : NSObject +{ + NSString* resourcePath; + NSURL* resourceURL; + CDVAudioPlayer* player; + CDVAudioRecorder* recorder; + NSNumber* volume; +} + +@property (nonatomic, strong) NSString* resourcePath; +@property (nonatomic, strong) NSURL* resourceURL; +@property (nonatomic, strong) CDVAudioPlayer* player; +@property (nonatomic, strong) NSNumber* volume; + +@property (nonatomic, strong) CDVAudioRecorder* recorder; + +@end + +@interface CDVSound : CDVPlugin <AVAudioPlayerDelegate, AVAudioRecorderDelegate> +{ + NSMutableDictionary* soundCache; + AVAudioSession* avSession; +} +@property (nonatomic, strong) NSMutableDictionary* soundCache; +@property (nonatomic, strong) AVAudioSession* avSession; + +- (void)startPlayingAudio:(CDVInvokedUrlCommand*)command; +- (void)pausePlayingAudio:(CDVInvokedUrlCommand*)command; +- (void)stopPlayingAudio:(CDVInvokedUrlCommand*)command; +- (void)seekToAudio:(CDVInvokedUrlCommand*)command; +- (void)release:(CDVInvokedUrlCommand*)command; +- (void)getCurrentPositionAudio:(CDVInvokedUrlCommand*)command; + +- (BOOL)hasAudioSession; + +// helper methods +- (NSURL*)urlForRecording:(NSString*)resourcePath; +- (NSURL*)urlForPlaying:(NSString*)resourcePath; + +- (CDVAudioFile*)audioFileForResource:(NSString*)resourcePath withId:(NSString*)mediaId doValidation:(BOOL)bValidate forRecording:(BOOL)bRecord; +- (BOOL)prepareToPlay:(CDVAudioFile*)audioFile withId:(NSString*)mediaId; +- (NSString*)createMediaErrorWithCode:(CDVMediaError)code message:(NSString*)message; + +- (void)startRecordingAudio:(CDVInvokedUrlCommand*)command; +- (void)stopRecordingAudio:(CDVInvokedUrlCommand*)command; + +- (void)setVolume:(CDVInvokedUrlCommand*)command; + +@end diff --git a/plugins/org.apache.cordova.media/src/ios/CDVSound.m b/plugins/org.apache.cordova.media/src/ios/CDVSound.m new file mode 100644 index 00000000..309b2e29 --- /dev/null +++ b/plugins/org.apache.cordova.media/src/ios/CDVSound.m @@ -0,0 +1,703 @@ +/* + 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. + */ + +#import "CDVSound.h" +#import "CDVFile.h" +#import <Cordova/NSArray+Comparisons.h> + +#define DOCUMENTS_SCHEME_PREFIX @"documents://" +#define HTTP_SCHEME_PREFIX @"http://" +#define HTTPS_SCHEME_PREFIX @"https://" +#define CDVFILE_PREFIX @"cdvfile://" +#define RECORDING_WAV @"wav" + +@implementation CDVSound + +@synthesize soundCache, avSession; + +// Maps a url for a resource path for recording +- (NSURL*)urlForRecording:(NSString*)resourcePath +{ + NSURL* resourceURL = nil; + NSString* filePath = nil; + NSString* docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; + + // first check for correct extension + if ([[resourcePath pathExtension] caseInsensitiveCompare:RECORDING_WAV] != NSOrderedSame) { + resourceURL = nil; + NSLog(@"Resource for recording must have %@ extension", RECORDING_WAV); + } else if ([resourcePath hasPrefix:DOCUMENTS_SCHEME_PREFIX]) { + // try to find Documents:// resources + filePath = [resourcePath stringByReplacingOccurrencesOfString:DOCUMENTS_SCHEME_PREFIX withString:[NSString stringWithFormat:@"%@/", docsPath]]; + NSLog(@"Will use resource '%@' from the documents folder with path = %@", resourcePath, filePath); + } else if ([resourcePath hasPrefix:CDVFILE_PREFIX]) { + CDVFile *filePlugin = [self.commandDelegate getCommandInstance:@"File"]; + CDVFilesystemURL *url = [CDVFilesystemURL fileSystemURLWithString:resourcePath]; + filePath = [filePlugin filesystemPathForURL:url]; + if (filePath == nil) { + resourceURL = [NSURL URLWithString:resourcePath]; + } + } else { + // if resourcePath is not from FileSystem put in tmp dir, else attempt to use provided resource path + NSString* tmpPath = [NSTemporaryDirectory()stringByStandardizingPath]; + BOOL isTmp = [resourcePath rangeOfString:tmpPath].location != NSNotFound; + BOOL isDoc = [resourcePath rangeOfString:docsPath].location != NSNotFound; + if (!isTmp && !isDoc) { + // put in temp dir + filePath = [NSString stringWithFormat:@"%@/%@", tmpPath, resourcePath]; + } else { + filePath = resourcePath; + } + } + + if (filePath != nil) { + // create resourceURL + resourceURL = [NSURL fileURLWithPath:filePath]; + } + return resourceURL; +} + +// Maps a url for a resource path for playing +// "Naked" resource paths are assumed to be from the www folder as its base +- (NSURL*)urlForPlaying:(NSString*)resourcePath +{ + NSURL* resourceURL = nil; + NSString* filePath = nil; + + // first try to find HTTP:// or Documents:// resources + + if ([resourcePath hasPrefix:HTTP_SCHEME_PREFIX] || [resourcePath hasPrefix:HTTPS_SCHEME_PREFIX]) { + // if it is a http url, use it + NSLog(@"Will use resource '%@' from the Internet.", resourcePath); + resourceURL = [NSURL URLWithString:resourcePath]; + } else if ([resourcePath hasPrefix:DOCUMENTS_SCHEME_PREFIX]) { + NSString* docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; + filePath = [resourcePath stringByReplacingOccurrencesOfString:DOCUMENTS_SCHEME_PREFIX withString:[NSString stringWithFormat:@"%@/", docsPath]]; + NSLog(@"Will use resource '%@' from the documents folder with path = %@", resourcePath, filePath); + } else if ([resourcePath hasPrefix:CDVFILE_PREFIX]) { + CDVFile *filePlugin = [self.commandDelegate getCommandInstance:@"File"]; + CDVFilesystemURL *url = [CDVFilesystemURL fileSystemURLWithString:resourcePath]; + filePath = [filePlugin filesystemPathForURL:url]; + if (filePath == nil) { + resourceURL = [NSURL URLWithString:resourcePath]; + } + } else { + // attempt to find file path in www directory or LocalFileSystem.TEMPORARY directory + filePath = [self.commandDelegate pathForResource:resourcePath]; + if (filePath == nil) { + // see if this exists in the documents/temp directory from a previous recording + NSString* testPath = [NSString stringWithFormat:@"%@/%@", [NSTemporaryDirectory()stringByStandardizingPath], resourcePath]; + if ([[NSFileManager defaultManager] fileExistsAtPath:testPath]) { + // inefficient as existence will be checked again below but only way to determine if file exists from previous recording + filePath = testPath; + NSLog(@"Will attempt to use file resource from LocalFileSystem.TEMPORARY directory"); + } else { + // attempt to use path provided + filePath = resourcePath; + NSLog(@"Will attempt to use file resource '%@'", filePath); + } + } else { + NSLog(@"Found resource '%@' in the web folder.", filePath); + } + } + // if the resourcePath resolved to a file path, check that file exists + if (filePath != nil) { + // create resourceURL + resourceURL = [NSURL fileURLWithPath:filePath]; + // try to access file + NSFileManager* fMgr = [NSFileManager defaultManager]; + if (![fMgr fileExistsAtPath:filePath]) { + resourceURL = nil; + NSLog(@"Unknown resource '%@'", resourcePath); + } + } + + return resourceURL; +} + +// Creates or gets the cached audio file resource object +- (CDVAudioFile*)audioFileForResource:(NSString*)resourcePath withId:(NSString*)mediaId doValidation:(BOOL)bValidate forRecording:(BOOL)bRecord +{ + BOOL bError = NO; + CDVMediaError errcode = MEDIA_ERR_NONE_SUPPORTED; + NSString* errMsg = @""; + NSString* jsString = nil; + CDVAudioFile* audioFile = nil; + NSURL* resourceURL = nil; + + if ([self soundCache] == nil) { + [self setSoundCache:[NSMutableDictionary dictionaryWithCapacity:1]]; + } else { + audioFile = [[self soundCache] objectForKey:mediaId]; + } + if (audioFile == nil) { + // validate resourcePath and create + if ((resourcePath == nil) || ![resourcePath isKindOfClass:[NSString class]] || [resourcePath isEqualToString:@""]) { + bError = YES; + errcode = MEDIA_ERR_ABORTED; + errMsg = @"invalid media src argument"; + } else { + audioFile = [[CDVAudioFile alloc] init]; + audioFile.resourcePath = resourcePath; + audioFile.resourceURL = nil; // validate resourceURL when actually play or record + [[self soundCache] setObject:audioFile forKey:mediaId]; + } + } + if (bValidate && (audioFile.resourceURL == nil)) { + if (bRecord) { + resourceURL = [self urlForRecording:resourcePath]; + } else { + resourceURL = [self urlForPlaying:resourcePath]; + } + if (resourceURL == nil) { + bError = YES; + errcode = MEDIA_ERR_ABORTED; + errMsg = [NSString stringWithFormat:@"Cannot use audio file from resource '%@'", resourcePath]; + } else { + audioFile.resourceURL = resourceURL; + } + } + + if (bError) { + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:errcode message:errMsg]]; + [self.commandDelegate evalJs:jsString]; + } + + return audioFile; +} + +// returns whether or not audioSession is available - creates it if necessary +- (BOOL)hasAudioSession +{ + BOOL bSession = YES; + + if (!self.avSession) { + NSError* error = nil; + + self.avSession = [AVAudioSession sharedInstance]; + if (error) { + // is not fatal if can't get AVAudioSession , just log the error + NSLog(@"error creating audio session: %@", [[error userInfo] description]); + self.avSession = nil; + bSession = NO; + } + } + return bSession; +} + +// helper function to create a error object string +- (NSString*)createMediaErrorWithCode:(CDVMediaError)code message:(NSString*)message +{ + NSMutableDictionary* errorDict = [NSMutableDictionary dictionaryWithCapacity:2]; + + [errorDict setObject:[NSNumber numberWithUnsignedInteger:code] forKey:@"code"]; + [errorDict setObject:message ? message:@"" forKey:@"message"]; + + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:errorDict options:0 error:nil]; + return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; +} + +- (void)create:(CDVInvokedUrlCommand*)command +{ + NSString* mediaId = [command argumentAtIndex:0]; + NSString* resourcePath = [command argumentAtIndex:1]; + + CDVAudioFile* audioFile = [self audioFileForResource:resourcePath withId:mediaId doValidation:NO forRecording:NO]; + + if (audioFile == nil) { + NSString* errorMessage = [NSString stringWithFormat:@"Failed to initialize Media file with path %@", resourcePath]; + NSString* jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_ABORTED message:errorMessage]]; + [self.commandDelegate evalJs:jsString]; + } else { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } +} + +- (void)setVolume:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + +#pragma unused(callbackId) + NSString* mediaId = [command argumentAtIndex:0]; + NSNumber* volume = [command argumentAtIndex:1 withDefault:[NSNumber numberWithFloat:1.0]]; + + if ([self soundCache] != nil) { + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + if (audioFile != nil) { + audioFile.volume = volume; + if (audioFile.player) { + audioFile.player.volume = [volume floatValue]; + } + [[self soundCache] setObject:audioFile forKey:mediaId]; + } + } + + // don't care for any callbacks +} + +- (void)startPlayingAudio:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + +#pragma unused(callbackId) + NSString* mediaId = [command argumentAtIndex:0]; + NSString* resourcePath = [command argumentAtIndex:1]; + NSDictionary* options = [command argumentAtIndex:2 withDefault:nil]; + + BOOL bError = NO; + NSString* jsString = nil; + + CDVAudioFile* audioFile = [self audioFileForResource:resourcePath withId:mediaId doValidation:YES forRecording:NO]; + if ((audioFile != nil) && (audioFile.resourceURL != nil)) { + if (audioFile.player == nil) { + bError = [self prepareToPlay:audioFile withId:mediaId]; + } + if (!bError) { + // audioFile.player != nil or player was successfully created + // get the audioSession and set the category to allow Playing when device is locked or ring/silent switch engaged + if ([self hasAudioSession]) { + NSError* __autoreleasing err = nil; + NSNumber* playAudioWhenScreenIsLocked = [options objectForKey:@"playAudioWhenScreenIsLocked"]; + BOOL bPlayAudioWhenScreenIsLocked = YES; + if (playAudioWhenScreenIsLocked != nil) { + bPlayAudioWhenScreenIsLocked = [playAudioWhenScreenIsLocked boolValue]; + } + + NSString* sessionCategory = bPlayAudioWhenScreenIsLocked ? AVAudioSessionCategoryPlayback : AVAudioSessionCategorySoloAmbient; + [self.avSession setCategory:sessionCategory error:&err]; + if (![self.avSession setActive:YES error:&err]) { + // other audio with higher priority that does not allow mixing could cause this to fail + NSLog(@"Unable to play audio: %@", [err localizedFailureReason]); + bError = YES; + } + } + if (!bError) { + NSLog(@"Playing audio sample '%@'", audioFile.resourcePath); + NSNumber* loopOption = [options objectForKey:@"numberOfLoops"]; + NSInteger numberOfLoops = 0; + if (loopOption != nil) { + numberOfLoops = [loopOption intValue] - 1; + } + audioFile.player.numberOfLoops = numberOfLoops; + if (audioFile.player.isPlaying) { + [audioFile.player stop]; + audioFile.player.currentTime = 0; + } + if (audioFile.volume != nil) { + audioFile.player.volume = [audioFile.volume floatValue]; + } + + [audioFile.player play]; + double position = round(audioFile.player.duration * 1000) / 1000; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%.3f);\n%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_DURATION, position, @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_STATE, MEDIA_RUNNING]; + [self.commandDelegate evalJs:jsString]; + } + } + if (bError) { + /* I don't see a problem playing previously recorded audio so removing this section - BG + NSError* error; + // try loading it one more time, in case the file was recorded previously + audioFile.player = [[ AVAudioPlayer alloc ] initWithContentsOfURL:audioFile.resourceURL error:&error]; + if (error != nil) { + NSLog(@"Failed to initialize AVAudioPlayer: %@\n", error); + audioFile.player = nil; + } else { + NSLog(@"Playing audio sample '%@'", audioFile.resourcePath); + audioFile.player.numberOfLoops = numberOfLoops; + [audioFile.player play]; + } */ + // error creating the session or player + // jsString = [NSString stringWithFormat: @"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, MEDIA_ERR_NONE_SUPPORTED]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_NONE_SUPPORTED message:nil]]; + [self.commandDelegate evalJs:jsString]; + } + } + // else audioFile was nil - error already returned from audioFile for resource + return; +} + +- (BOOL)prepareToPlay:(CDVAudioFile*)audioFile withId:(NSString*)mediaId +{ + BOOL bError = NO; + NSError* __autoreleasing playerError = nil; + + // create the player + NSURL* resourceURL = audioFile.resourceURL; + + if ([resourceURL isFileURL]) { + audioFile.player = [[CDVAudioPlayer alloc] initWithContentsOfURL:resourceURL error:&playerError]; + } else { + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:resourceURL]; + NSString* userAgent = [self.commandDelegate userAgent]; + if (userAgent) { + [request setValue:userAgent forHTTPHeaderField:@"User-Agent"]; + } + + NSURLResponse* __autoreleasing response = nil; + NSData* data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&playerError]; + if (playerError) { + NSLog(@"Unable to download audio from: %@", [resourceURL absoluteString]); + } else { + // bug in AVAudioPlayer when playing downloaded data in NSData - we have to download the file and play from disk + CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault); + CFStringRef uuidString = CFUUIDCreateString(kCFAllocatorDefault, uuidRef); + NSString* filePath = [NSString stringWithFormat:@"%@/%@", [NSTemporaryDirectory()stringByStandardizingPath], uuidString]; + CFRelease(uuidString); + CFRelease(uuidRef); + + [data writeToFile:filePath atomically:YES]; + NSURL* fileURL = [NSURL fileURLWithPath:filePath]; + audioFile.player = [[CDVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&playerError]; + } + } + + if (playerError != nil) { + NSLog(@"Failed to initialize AVAudioPlayer: %@\n", [playerError localizedDescription]); + audioFile.player = nil; + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + } + bError = YES; + } else { + audioFile.player.mediaId = mediaId; + audioFile.player.delegate = self; + bError = ![audioFile.player prepareToPlay]; + } + return bError; +} + +- (void)stopPlayingAudio:(CDVInvokedUrlCommand*)command +{ + NSString* mediaId = [command argumentAtIndex:0]; + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + NSString* jsString = nil; + + if ((audioFile != nil) && (audioFile.player != nil)) { + NSLog(@"Stopped playing audio sample '%@'", audioFile.resourcePath); + [audioFile.player stop]; + audioFile.player.currentTime = 0; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_STATE, MEDIA_STOPPED]; + } // ignore if no media playing + if (jsString) { + [self.commandDelegate evalJs:jsString]; + } +} + +- (void)pausePlayingAudio:(CDVInvokedUrlCommand*)command +{ + NSString* mediaId = [command argumentAtIndex:0]; + NSString* jsString = nil; + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + + if ((audioFile != nil) && (audioFile.player != nil)) { + NSLog(@"Paused playing audio sample '%@'", audioFile.resourcePath); + [audioFile.player pause]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_STATE, MEDIA_PAUSED]; + } + // ignore if no media playing + + if (jsString) { + [self.commandDelegate evalJs:jsString]; + } +} + +- (void)seekToAudio:(CDVInvokedUrlCommand*)command +{ + // args: + // 0 = Media id + // 1 = path to resource + // 2 = seek to location in milliseconds + + NSString* mediaId = [command argumentAtIndex:0]; + + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + double position = [[command argumentAtIndex:1] doubleValue]; + + if ((audioFile != nil) && (audioFile.player != nil)) { + NSString* jsString; + double posInSeconds = position / 1000; + if (posInSeconds >= audioFile.player.duration) { + // The seek is past the end of file. Stop media and reset to beginning instead of seeking past the end. + [audioFile.player stop]; + audioFile.player.currentTime = 0; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%.3f);\n%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_POSITION, 0.0, @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_STATE, MEDIA_STOPPED]; + // NSLog(@"seekToEndJsString=%@",jsString); + } else { + audioFile.player.currentTime = posInSeconds; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%f);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_POSITION, posInSeconds]; + // NSLog(@"seekJsString=%@",jsString); + } + + [self.commandDelegate evalJs:jsString]; + } +} + +- (void)release:(CDVInvokedUrlCommand*)command +{ + NSString* mediaId = [command argumentAtIndex:0]; + + if (mediaId != nil) { + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + if (audioFile != nil) { + if (audioFile.player && [audioFile.player isPlaying]) { + [audioFile.player stop]; + } + if (audioFile.recorder && [audioFile.recorder isRecording]) { + [audioFile.recorder stop]; + } + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + self.avSession = nil; + } + [[self soundCache] removeObjectForKey:mediaId]; + NSLog(@"Media with id %@ released", mediaId); + } + } +} + +- (void)getCurrentPositionAudio:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSString* mediaId = [command argumentAtIndex:0]; + +#pragma unused(mediaId) + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + double position = -1; + + if ((audioFile != nil) && (audioFile.player != nil) && [audioFile.player isPlaying]) { + position = round(audioFile.player.currentTime * 1000) / 1000; + } + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble:position]; + + NSString* jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%.3f);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_POSITION, position]; + [self.commandDelegate evalJs:jsString]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; +} + +- (void)startRecordingAudio:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + +#pragma unused(callbackId) + + NSString* mediaId = [command argumentAtIndex:0]; + CDVAudioFile* audioFile = [self audioFileForResource:[command argumentAtIndex:1] withId:mediaId doValidation:YES forRecording:YES]; + __block NSString* jsString = nil; + __block NSString* errorMsg = @""; + + if ((audioFile != nil) && (audioFile.resourceURL != nil)) { + void (^startRecording)(void) = ^{ + NSError* __autoreleasing error = nil; + + if (audioFile.recorder != nil) { + [audioFile.recorder stop]; + audioFile.recorder = nil; + } + // get the audioSession and set the category to allow recording when device is locked or ring/silent switch engaged + if ([self hasAudioSession]) { + if (![self.avSession.category isEqualToString:AVAudioSessionCategoryPlayAndRecord]) { + [self.avSession setCategory:AVAudioSessionCategoryRecord error:nil]; + } + + if (![self.avSession setActive:YES error:&error]) { + // other audio with higher priority that does not allow mixing could cause this to fail + errorMsg = [NSString stringWithFormat:@"Unable to record audio: %@", [error localizedFailureReason]]; + // jsString = [NSString stringWithFormat: @"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, MEDIA_ERR_ABORTED]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_ABORTED message:errorMsg]]; + [self.commandDelegate evalJs:jsString]; + return; + } + } + + // create a new recorder for each start record + audioFile.recorder = [[CDVAudioRecorder alloc] initWithURL:audioFile.resourceURL settings:nil error:&error]; + + bool recordingSuccess = NO; + if (error == nil) { + audioFile.recorder.delegate = self; + audioFile.recorder.mediaId = mediaId; + recordingSuccess = [audioFile.recorder record]; + if (recordingSuccess) { + NSLog(@"Started recording audio sample '%@'", audioFile.resourcePath); + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_STATE, MEDIA_RUNNING]; + [self.commandDelegate evalJs:jsString]; + } + } + + if ((error != nil) || (recordingSuccess == NO)) { + if (error != nil) { + errorMsg = [NSString stringWithFormat:@"Failed to initialize AVAudioRecorder: %@\n", [error localizedFailureReason]]; + } else { + errorMsg = @"Failed to start recording using AVAudioRecorder"; + } + audioFile.recorder = nil; + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + } + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_ABORTED message:errorMsg]]; + [self.commandDelegate evalJs:jsString]; + } + }; + + SEL rrpSel = NSSelectorFromString(@"requestRecordPermission:"); + if ([self hasAudioSession] && [self.avSession respondsToSelector:rrpSel]) + { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [self.avSession performSelector:rrpSel withObject:^(BOOL granted){ + if (granted) { + startRecording(); + } else { + NSString* msg = @"Error creating audio session, microphone permission denied."; + NSLog(@"%@", msg); + audioFile.recorder = nil; + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + } + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_ABORTED message:msg]]; + [self.commandDelegate evalJs:jsString]; + } + }]; +#pragma clang diagnostic pop + } else { + startRecording(); + } + + } else { + // file did not validate + NSString* errorMsg = [NSString stringWithFormat:@"Could not record audio at '%@'", audioFile.resourcePath]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_ABORTED message:errorMsg]]; + [self.commandDelegate evalJs:jsString]; + } +} + +- (void)stopRecordingAudio:(CDVInvokedUrlCommand*)command +{ + NSString* mediaId = [command argumentAtIndex:0]; + + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + NSString* jsString = nil; + + if ((audioFile != nil) && (audioFile.recorder != nil)) { + NSLog(@"Stopped recording audio sample '%@'", audioFile.resourcePath); + [audioFile.recorder stop]; + // no callback - that will happen in audioRecorderDidFinishRecording + } + // ignore if no media recording + if (jsString) { + [self.commandDelegate evalJs:jsString]; + } +} + +- (void)audioRecorderDidFinishRecording:(AVAudioRecorder*)recorder successfully:(BOOL)flag +{ + CDVAudioRecorder* aRecorder = (CDVAudioRecorder*)recorder; + NSString* mediaId = aRecorder.mediaId; + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + NSString* jsString = nil; + + if (audioFile != nil) { + NSLog(@"Finished recording audio sample '%@'", audioFile.resourcePath); + } + if (flag) { + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_STATE, MEDIA_STOPPED]; + } else { + // jsString = [NSString stringWithFormat: @"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, MEDIA_ERR_DECODE]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_DECODE message:nil]]; + } + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + } + [self.commandDelegate evalJs:jsString]; +} + +- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer*)player successfully:(BOOL)flag +{ + CDVAudioPlayer* aPlayer = (CDVAudioPlayer*)player; + NSString* mediaId = aPlayer.mediaId; + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + NSString* jsString = nil; + + if (audioFile != nil) { + NSLog(@"Finished playing audio sample '%@'", audioFile.resourcePath); + } + if (flag) { + audioFile.player.currentTime = 0; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_STATE, MEDIA_STOPPED]; + } else { + // jsString = [NSString stringWithFormat: @"%@(\"%@\",%d,%d);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, MEDIA_ERR_DECODE]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('org.apache.cordova.media.Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_DECODE message:nil]]; + } + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + } + [self.commandDelegate evalJs:jsString]; +} + +- (void)onMemoryWarning +{ + [[self soundCache] removeAllObjects]; + [self setSoundCache:nil]; + [self setAvSession:nil]; + + [super onMemoryWarning]; +} + +- (void)dealloc +{ + [[self soundCache] removeAllObjects]; +} + +- (void)onReset +{ + for (CDVAudioFile* audioFile in [[self soundCache] allValues]) { + if (audioFile != nil) { + if (audioFile.player != nil) { + [audioFile.player stop]; + audioFile.player.currentTime = 0; + } + if (audioFile.recorder != nil) { + [audioFile.recorder stop]; + } + } + } + + [[self soundCache] removeAllObjects]; +} + +@end + +@implementation CDVAudioFile + +@synthesize resourcePath; +@synthesize resourceURL; +@synthesize player, volume; +@synthesize recorder; + +@end +@implementation CDVAudioPlayer +@synthesize mediaId; + +@end + +@implementation CDVAudioRecorder +@synthesize mediaId; + +@end diff --git a/plugins/org.apache.cordova.media/src/tizen/MediaProxy.js b/plugins/org.apache.cordova.media/src/tizen/MediaProxy.js new file mode 100644 index 00000000..c2ee4b07 --- /dev/null +++ b/plugins/org.apache.cordova.media/src/tizen/MediaProxy.js @@ -0,0 +1,223 @@ +/* + * + * 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. + * +*/ + +var cordova = require('cordova'), + Media = require('org.apache.cordova.media.Media'); + +var MediaError = require('org.apache.cordova.media.MediaError'), + audioObjects = {}; + +module.exports = { + // Initiates the audio file + create:function(successCallback, errorCallback, args) { + var id = args[0], src = args[1]; + + console.log("media::create() - id =" + id + ", src =" + src); + + audioObjects[id] = new Audio(src); + + audioObjects[id].onStalledCB = function () { + console.log("media::onStalled()"); + + audioObjects[id].timer = window.setTimeout( + function () { + audioObjects[id].pause(); + + if (audioObjects[id].currentTime !== 0) + audioObjects[id].currentTime = 0; + + console.log("media::onStalled() - MEDIA_ERROR -> " + MediaError.MEDIA_ERR_ABORTED); + + var err = new MediaError(MediaError.MEDIA_ERR_ABORTED, "Stalled"); + + Media.onStatus(id, Media.MEDIA_ERROR, err); + }, + 2000); + }; + + audioObjects[id].onEndedCB = function () { + console.log("media::onEndedCB() - MEDIA_STATE -> MEDIA_STOPPED"); + + Media.onStatus(id, Media.MEDIA_STATE, Media.MEDIA_STOPPED); + }; + + audioObjects[id].onErrorCB = function () { + console.log("media::onErrorCB() - MEDIA_ERROR -> " + event.srcElement.error); + + Media.onStatus(id, Media.MEDIA_ERROR, event.srcElement.error); + }; + + audioObjects[id].onPlayCB = function () { + console.log("media::onPlayCB() - MEDIA_STATE -> MEDIA_STARTING"); + + Media.onStatus(id, Media.MEDIA_STATE, Media.MEDIA_STARTING); + }; + + audioObjects[id].onPlayingCB = function () { + console.log("media::onPlayingCB() - MEDIA_STATE -> MEDIA_RUNNING"); + + Media.onStatus(id, Media.MEDIA_STATE, Media.MEDIA_RUNNING); + }; + + audioObjects[id].onDurationChangeCB = function () { + console.log("media::onDurationChangeCB() - MEDIA_DURATION -> " + audioObjects[id].duration); + + Media.onStatus(id, Media.MEDIA_DURATION, audioObjects[id].duration); + }; + + audioObjects[id].onTimeUpdateCB = function () { + console.log("media::onTimeUpdateCB() - MEDIA_POSITION -> " + audioObjects[id].currentTime); + + Media.onStatus(id, Media.MEDIA_POSITION, audioObjects[id].currentTime); + }; + + audioObjects[id].onCanPlayCB = function () { + console.log("media::onCanPlayCB()"); + + window.clearTimeout(audioObjects[id].timer); + + audioObjects[id].play(); + }; + + }, + + // Start playing the audio + startPlayingAudio:function(successCallback, errorCallback, args) { + var id = args[0], src = args[1], options = args[2]; + + console.log("media::startPlayingAudio() - id =" + id + ", src =" + src + ", options =" + options); + + audioObjects[id].addEventListener('canplay', audioObjects[id].onCanPlayCB); + audioObjects[id].addEventListener('ended', audioObjects[id].onEndedCB); + audioObjects[id].addEventListener('timeupdate', audioObjects[id].onTimeUpdateCB); + audioObjects[id].addEventListener('durationchange', audioObjects[id].onDurationChangeCB); + audioObjects[id].addEventListener('playing', audioObjects[id].onPlayingCB); + audioObjects[id].addEventListener('play', audioObjects[id].onPlayCB); + audioObjects[id].addEventListener('error', audioObjects[id].onErrorCB); + audioObjects[id].addEventListener('stalled', audioObjects[id].onStalledCB); + + audioObjects[id].play(); + }, + + // Stops the playing audio + stopPlayingAudio:function(successCallback, errorCallback, args) { + var id = args[0]; + + window.clearTimeout(audioObjects[id].timer); + + audioObjects[id].pause(); + + if (audioObjects[id].currentTime !== 0) + audioObjects[id].currentTime = 0; + + console.log("media::stopPlayingAudio() - MEDIA_STATE -> MEDIA_STOPPED"); + + Media.onStatus(id, Media.MEDIA_STATE, Media.MEDIA_STOPPED); + + audioObjects[id].removeEventListener('canplay', audioObjects[id].onCanPlayCB); + audioObjects[id].removeEventListener('ended', audioObjects[id].onEndedCB); + audioObjects[id].removeEventListener('timeupdate', audioObjects[id].onTimeUpdateCB); + audioObjects[id].removeEventListener('durationchange', audioObjects[id].onDurationChangeCB); + audioObjects[id].removeEventListener('playing', audioObjects[id].onPlayingCB); + audioObjects[id].removeEventListener('play', audioObjects[id].onPlayCB); + audioObjects[id].removeEventListener('error', audioObjects[id].onErrorCB); + audioObjects[id].removeEventListener('error', audioObjects[id].onStalledCB); + }, + + // Seeks to the position in the audio + seekToAudio:function(successCallback, errorCallback, args) { + var id = args[0], milliseconds = args[1]; + + console.log("media::seekToAudio()"); + + audioObjects[id].currentTime = milliseconds; + successCallback( audioObjects[id].currentTime); + }, + + // Pauses the playing audio + pausePlayingAudio:function(successCallback, errorCallback, args) { + var id = args[0]; + + console.log("media::pausePlayingAudio() - MEDIA_STATE -> MEDIA_PAUSED"); + + audioObjects[id].pause(); + + Media.onStatus(id, Media.MEDIA_STATE, Media.MEDIA_PAUSED); + }, + + // Gets current position in the audio + getCurrentPositionAudio:function(successCallback, errorCallback, args) { + var id = args[0]; + console.log("media::getCurrentPositionAudio()"); + successCallback(audioObjects[id].currentTime); + }, + + // Start recording audio + startRecordingAudio:function(successCallback, errorCallback, args) { + var id = args[0], src = args[1]; + + console.log("media::startRecordingAudio() - id =" + id + ", src =" + src); + + function gotStreamCB(stream) { + audioObjects[id].src = webkitURL.createObjectURL(stream); + console.log("media::startRecordingAudio() - stream CB"); + } + + function gotStreamFailedCB(error) { + console.log("media::startRecordingAudio() - error CB:" + error.toString()); + } + + if (navigator.webkitGetUserMedia) { + audioObjects[id] = new Audio(); + navigator.webkitGetUserMedia('audio', gotStreamCB, gotStreamFailedCB); + } else { + console.log("webkitGetUserMedia not supported"); + } + successCallback(); + }, + + // Stop recording audio + stopRecordingAudio:function(successCallback, errorCallback, args) { + var id = args[0]; + + console.log("media::stopRecordingAudio() - id =" + id); + + audioObjects[id].pause(); + successCallback(); + }, + + // Release the media object + release:function(successCallback, errorCallback, args) { + var id = args[0]; + window.clearTimeout(audioObjects[id].timer); + console.log("media::release()"); + }, + + setVolume:function(successCallback, errorCallback, args) { + var id = args[0], volume = args[1]; + + console.log("media::setVolume()"); + + audioObjects[id].volume = volume; + } +}; + +require("cordova/tizen/commandProxy").add("Media", module.exports); diff --git a/plugins/org.apache.cordova.media/src/ubuntu/media.cpp b/plugins/org.apache.cordova.media/src/ubuntu/media.cpp new file mode 100644 index 00000000..2814b5b3 --- /dev/null +++ b/plugins/org.apache.cordova.media/src/ubuntu/media.cpp @@ -0,0 +1,128 @@ +/* + * + * 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. + * +*/ + +#include "media.h" + +void Media::create(int scId, int ecId, const QString &id, const QString &src) { + Q_UNUSED(scId); + Q_UNUSED(ecId); + + if (_id2Player.find(id) != _id2Player.end()) { + _id2Player[id]->stop(); + _id2Player.remove(id); + } + + _id2Player[id] = QSharedPointer<Player>(new Player(id, src, this)); +} + +void Media::relase(int scId, int ecId, const QString &id) { + Q_UNUSED(scId); + Q_UNUSED(ecId); + + if (_id2Player.find(id) == _id2Player.end()) + return; + _id2Player.remove(id); +} + +void Media::startPlayingAudio(int scId, int ecId, const QString &id, const QString &src, QVariantMap options) { + Q_UNUSED(scId); + Q_UNUSED(ecId); + Q_UNUSED(src); + Q_UNUSED(options); + + if (_id2Player.find(id) == _id2Player.end()) + return; + QSharedPointer<Player> player = _id2Player[id]; + player->play(); +} + +void Media::pausePlayingAudio(int scId, int ecId, const QString &id) { + Q_UNUSED(scId); + Q_UNUSED(ecId); + + if (_id2Player.find(id) == _id2Player.end()) + return; + QSharedPointer<Player> player = _id2Player[id]; + player->pause(); +} + +void Media::stopPlayingAudio(int scId, int ecId, const QString &id) { + Q_UNUSED(scId); + Q_UNUSED(ecId); + + if (_id2Player.find(id) == _id2Player.end()) + return; + QSharedPointer<Player> player = _id2Player[id]; + player->stop(); +} + +void Media::startRecordingAudio(int scId, int ecId, const QString &id, const QString &src) { + Q_UNUSED(scId); + Q_UNUSED(ecId); + Q_UNUSED(src); + + if (_id2Player.find(id) == _id2Player.end()) + return; + QSharedPointer<Player> player = _id2Player[id]; + player->startRecording(); +} + +void Media::stopRecordingAudio(int scId, int ecId, const QString &id) { + Q_UNUSED(scId); + Q_UNUSED(ecId); + + if (_id2Player.find(id) == _id2Player.end()) + return; + QSharedPointer<Player> player = _id2Player[id]; + player->stopRecording(); +} + +void Media::getCurrentPositionAudio(int scId, int ecId, const QString &id) { + Q_UNUSED(ecId); + + if (_id2Player.find(id) == _id2Player.end()) + return; + + QSharedPointer<Player> player = _id2Player[id]; + double position = player->getPosition(); + this->cb(scId, position); +} + +void Media::seekToAudio(int scId, int ecId, const QString &id, qint64 position) { + Q_UNUSED(scId); + Q_UNUSED(ecId); + + if (_id2Player.find(id) == _id2Player.end()) + return; + + QSharedPointer<Player> player = _id2Player[id]; + player->seekTo(position); +} + +void Media::setVolume(int scId, int ecId, const QString &id, int volume) { + Q_UNUSED(scId); + Q_UNUSED(ecId); + + if (_id2Player.find(id) == _id2Player.end()) + return; + QSharedPointer<Player> player = _id2Player[id]; + player->setVolume(volume); +} diff --git a/plugins/org.apache.cordova.media/src/ubuntu/media.h b/plugins/org.apache.cordova.media/src/ubuntu/media.h new file mode 100644 index 00000000..c1f37122 --- /dev/null +++ b/plugins/org.apache.cordova.media/src/ubuntu/media.h @@ -0,0 +1,267 @@ +/* + * + * 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. + * +*/ + +#ifndef MEDIA_H_789768978 +#define MEDIA_H_789768978 + +#include <QtMultimedia/QMediaPlayer> +#include <QtCore> +#include <QAudioRecorder> +#include <QtMultimedia/QAudioEncoderSettings> + +#include <cplugin.h> +#include <cordova.h> + +class Player; + +class Media: public CPlugin { + Q_OBJECT +public: + explicit Media(Cordova *cordova): CPlugin(cordova) { + } + + virtual const QString fullName() override { + return Media::fullID(); + } + + virtual const QString shortName() override { + return "Media"; + } + + static const QString fullID() { + return "Media"; + } + + enum State { + MEDIA_NONE = 0, + MEDIA_STARTING = 1, + MEDIA_RUNNING = 2, + MEDIA_PAUSED = 3, + MEDIA_STOPPED = 4 + }; + enum ErrorCode { + MEDIA_ERR_NONE_ACTIVE = 0, + MEDIA_ERR_ABORTED = 1, + MEDIA_ERR_NETWORK = 2, + MEDIA_ERR_DECODE = 3, + MEDIA_ERR_NONE_SUPPORTED = 4 + }; + + void execJS(const QString &js) { + m_cordova->execJS(js); + } +public slots: + void create(int scId, int ecId, const QString &id, const QString &src); + void relase(int scId, int ecId, const QString &id); + + void startRecordingAudio(int scId, int ecId, const QString &id, const QString &src); + void stopRecordingAudio(int scId, int ecId, const QString &id); + + void startPlayingAudio(int scId, int ecId, const QString &id, const QString &src, QVariantMap options); + void pausePlayingAudio(int scId, int ecId, const QString &id); + void stopPlayingAudio(int scId, int ecId, const QString &id); + void getCurrentPositionAudio(int scId, int ecId, const QString &id); + void seekToAudio(int scId, int ecId, const QString &id, qint64 position); + void setVolume(int scId, int ecId, const QString &id, int volume); + +private: + QMap<QString, QSharedPointer<Player> > _id2Player; +}; + +class Player: public QObject { + Q_OBJECT +public: + Player(const QString &id, QString src, Media *plugin): + _state(Media::MEDIA_NONE), + _src(src), + _mode(MODE_NONE), + _plugin(plugin), + _id(id), + _stateChanged(false) { + QUrl url(src, QUrl::TolerantMode); + + if (url.scheme().isEmpty()) { + QAudioEncoderSettings audioSettings; + + _recorder.setEncodingSettings(audioSettings); + _recorder.setOutputLocation(QFileInfo(src).absoluteFilePath()); + + _player.setMedia(QUrl::fromLocalFile(QFileInfo(src).absoluteFilePath())); + } else { + _player.setMedia(url); + } + QObject::connect(&_player, SIGNAL(mediaStatusChanged(QMediaPlayer::MediaStatus)), this, SLOT(onMediaStatusChanged(QMediaPlayer::MediaStatus))); + QObject::connect(&_recorder, SIGNAL(error(QMediaRecorder::Error)), this, SLOT(onError(QMediaRecorder::Error))); + + connect(&_timer, SIGNAL(timeout()), this, SLOT(reportPosition())); + } + + void startRecording() { + if (recordMode() && _state != Media::MEDIA_RUNNING) { + _recorder.record(); + setState(Media::MEDIA_RUNNING); + } + } + void stopRecording() { + if (recordMode() && _state == Media::MEDIA_RUNNING) { + _recorder.stop(); + setState(Media::MEDIA_STOPPED); + } + } + + void setVolume(int volume) { + _player.setVolume(volume); + } + + void play() { + if (playMode() && _state != Media::MEDIA_RUNNING) { + _player.play(); + setState(Media::MEDIA_RUNNING); + } + } + void pause() { + if (playMode() && _state == Media::MEDIA_RUNNING) { + _player.pause(); + setState(Media::MEDIA_PAUSED); + } + } + void stop() { + if (playMode() && (_state == Media::MEDIA_RUNNING || _state == Media::MEDIA_PAUSED)) { + _player.stop(); + setState(Media::MEDIA_STOPPED); + } + } + double getDuration() { + if (_mode == MODE_NONE || _player.duration() == -1) + return -1; + if (_mode != MODE_PLAY) + return -2; + return static_cast<double>(_player.duration()) / 1000.0; + } + double getPosition() { + if (_mode != MODE_PLAY) + return -1; + return static_cast<double>(_player.position()) / 1000.0; + } + bool seekTo(qint64 position) { + if (!_player.isSeekable()) + return false; + _player.setPosition(position * 1000); + return true; + } +private slots: + void reportPosition() { + double position = getPosition(); + _plugin->execJS(QString("Media.onStatus('%1', Media.MEDIA_POSITION, %2)") + .arg(_id).arg(position)); + double duration = getDuration(); + _plugin->execJS(QString("Media.onStatus('%1', Media.MEDIA_DURATION, %2)") + .arg(_id).arg(duration)); + + if (_stateChanged && !(_state == Media::MEDIA_RUNNING && (duration == -1 || position == 0))) { + qCritical() << _id << "POSITION" << position << ":" << duration; + _stateChanged = false; + _plugin->execJS(QString("Media.onStatus('%1', Media.MEDIA_STATE, %2)").arg(_id).arg(_state)); + } + } + + void onMediaStatusChanged(QMediaPlayer::MediaStatus status) { + if (status == QMediaPlayer::InvalidMedia) { + reportError(Media::MEDIA_ERR_ABORTED, "AudioPlayer Error: The current media cannot be played."); + setState(Media::MEDIA_STOPPED); + } + if (status == QMediaPlayer::EndOfMedia) { + setState(Media::MEDIA_STOPPED); + seekTo(0); + } + } + void onError(QMediaRecorder::Error) { + reportError(Media::MEDIA_ERR_NONE_SUPPORTED, "AudioPlayer Error: Device is not ready or not available."); + setState(Media::MEDIA_STOPPED); + } + +private: + void reportError(int code, const QString &descr) { + Q_UNUSED(descr); + _plugin->execJS(QString("Media.onStatus('%1', Media.MEDIA_ERROR, {code: %2})") + .arg(_id).arg(code)); + } + + bool playMode() { + switch (_mode) { + case Player::MODE_NONE: + _mode = MODE_PLAY; + break; + case Player::MODE_PLAY: + break; + case Player::MODE_RECORD: + reportError(Media::MEDIA_ERR_NONE_SUPPORTED, "AudioPlayer Error: Can't play in record mode."); + return false; + break; + } + return true; + } + + bool recordMode() { + switch (_mode) { + case Player::MODE_NONE: + if (_recorder.outputLocation().isEmpty()) { + reportError(Media::MEDIA_ERR_NONE_SUPPORTED, "AudioPlayer Error: unsupported output location."); + return false; + } + _mode = MODE_RECORD; + break; + case Player::MODE_PLAY: + reportError(Media::MEDIA_ERR_NONE_SUPPORTED, "AudioPlayer Error: Can't play in play mode."); + return false; + break; + case Player::MODE_RECORD: + break; + } + return true; + } + + void setState(Media::State state) { + _state = state; + _stateChanged = true; + _timer.start(250); + } + + QMediaPlayer _player; + + QAudioRecorder _recorder; + QTimer _timer; + + Media::State _state; + QString _src; + enum Mode { + MODE_NONE, + MODE_PLAY, + MODE_RECORD + }; + Mode _mode; + Media *_plugin; + QString _id; + + bool _stateChanged; +}; + +#endif diff --git a/plugins/org.apache.cordova.media/src/windows8/MediaProxy.js b/plugins/org.apache.cordova.media/src/windows8/MediaProxy.js new file mode 100644 index 00000000..e7ec51b7 --- /dev/null +++ b/plugins/org.apache.cordova.media/src/windows8/MediaProxy.js @@ -0,0 +1,217 @@ +/* + * + * 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. + * +*/ + +/*global Windows:true */ + +var cordova = require('cordova'), + Media = require('org.apache.cordova.media.Media'); + +var MediaError = require('org.apache.cordova.media.MediaError'); + +var recordedFile; + +module.exports = { + mediaCaptureMrg:null, + + // Initiates the audio file + create:function(win, lose, args) { + var id = args[0]; + var src = args[1]; + var thisM = Media.get(id); + Media.onStatus(id, Media.MEDIA_STATE, Media.MEDIA_STARTING); + + Media.prototype.node = null; + + var fn = src.split('.').pop(); // gets the file extension + if (thisM.node === null) { + if (fn === 'mp3' || fn === 'wma' || fn === 'wav' || + fn === 'cda' || fn === 'adx' || fn === 'wm' || + fn === 'm3u' || fn === 'wmx' || fn === 'm4a') { + thisM.node = new Audio(src); + thisM.node.load(); + + var getDuration = function () { + var dur = thisM.node.duration; + if (isNaN(dur)) { + dur = -1; + } + Media.onStatus(id, Media.MEDIA_DURATION, dur); + }; + + thisM.node.onloadedmetadata = getDuration; + getDuration(); + } + else { + lose && lose({code:MediaError.MEDIA_ERR_ABORTED}); + } + } + }, + + // Start playing the audio + startPlayingAudio:function(win, lose, args) { + var id = args[0]; + //var src = args[1]; + //var options = args[2]; + + var thisM = Media.get(id); + // if Media was released, then node will be null and we need to create it again + if (!thisM.node) { + module.exports.create(win, lose, args); + } + + Media.onStatus(id, Media.MEDIA_STATE, Media.MEDIA_RUNNING); + + thisM.node.play(); + }, + + // Stops the playing audio + stopPlayingAudio:function(win, lose, args) { + var id = args[0]; + try { + (Media.get(id)).node.pause(); + (Media.get(id)).node.currentTime = 0; + Media.onStatus(id, Media.MEDIA_STATE, Media.MEDIA_STOPPED); + win(); + } catch (err) { + lose("Failed to stop: "+err); + } + }, + + // Seeks to the position in the audio + seekToAudio:function(win, lose, args) { + var id = args[0]; + var milliseconds = args[1]; + try { + (Media.get(id)).node.currentTime = milliseconds / 1000; + win(); + } catch (err) { + lose("Failed to seek: "+err); + } + }, + + // Pauses the playing audio + pausePlayingAudio:function(win, lose, args) { + var id = args[0]; + var thisM = Media.get(id); + try { + thisM.node.pause(); + Media.onStatus(id, Media.MEDIA_STATE, Media.MEDIA_PAUSED); + } catch (err) { + lose("Failed to pause: "+err); + } + }, + + // Gets current position in the audio + getCurrentPositionAudio:function(win, lose, args) { + var id = args[0]; + try { + var p = (Media.get(id)).node.currentTime; + Media.onStatus(id, Media.MEDIA_POSITION, p); + win(p); + } catch (err) { + lose(err); + } + }, + + // Start recording audio + startRecordingAudio:function(win, lose, args) { + var id = args[0]; + var src = args[1]; + + var normalizedSrc = src.replace(/\//g, '\\'); + var destPath = normalizedSrc.substr(0, normalizedSrc.lastIndexOf('\\')); + var destFileName = normalizedSrc.replace(destPath + '\\', ''); + + // Initialize device + Media.prototype.mediaCaptureMgr = null; + var thisM = (Media.get(id)); + var captureInitSettings = new Windows.Media.Capture.MediaCaptureInitializationSettings(); + captureInitSettings.streamingCaptureMode = Windows.Media.Capture.StreamingCaptureMode.audio; + thisM.mediaCaptureMgr = new Windows.Media.Capture.MediaCapture(); + thisM.mediaCaptureMgr.addEventListener("failed", lose); + + thisM.mediaCaptureMgr.initializeAsync(captureInitSettings).done(function (result) { + thisM.mediaCaptureMgr.addEventListener("recordlimitationexceeded", lose); + thisM.mediaCaptureMgr.addEventListener("failed", lose); + + // Start recording + Windows.Storage.ApplicationData.current.temporaryFolder.createFileAsync(destFileName, Windows.Storage.CreationCollisionOption.replaceExisting).done(function (newFile) { + recordedFile = newFile; + var encodingProfile = null; + switch (newFile.fileType) { + case '.m4a': + encodingProfile = Windows.Media.MediaProperties.MediaEncodingProfile.createM4a(Windows.Media.MediaProperties.AudioEncodingQuality.auto); + break; + case '.mp3': + encodingProfile = Windows.Media.MediaProperties.MediaEncodingProfile.createMp3(Windows.Media.MediaProperties.AudioEncodingQuality.auto); + break; + case '.wma': + encodingProfile = Windows.Media.MediaProperties.MediaEncodingProfile.createWma(Windows.Media.MediaProperties.AudioEncodingQuality.auto); + break; + default: + lose("Invalid file type for record"); + break; + } + thisM.mediaCaptureMgr.startRecordToStorageFileAsync(encodingProfile, newFile).done(win, lose); + }, lose); + }, lose); + }, + + // Stop recording audio + stopRecordingAudio:function(win, lose, args) { + var id = args[0]; + var thisM = Media.get(id); + + var normalizedSrc = thisM.src.replace(/\//g, '\\'); + var destPath = normalizedSrc.substr(0, normalizedSrc.lastIndexOf('\\')); + var destFileName = normalizedSrc.replace(destPath + '\\', ''); + + thisM.mediaCaptureMgr.stopRecordAsync().done(function () { + if (destPath) { + Windows.Storage.StorageFolder.getFolderFromPathAsync(destPath).done(function(destFolder) { + recordedFile.copyAsync(destFolder, destFileName, Windows.Storage.CreationCollisionOption.replaceExisting).done(win, lose); + }, lose); + } else { + // if path is not defined, we leave recorded file in temporary folder (similar to iOS) + win(); + } + }, lose); + }, + + // Release the media object + release:function(win, lose, args) { + var id = args[0]; + var thisM = Media.get(id); + try { + delete thisM.node; + } catch (err) { + lose("Failed to release: "+err); + } + }, + setVolume:function(win, lose, args) { + var id = args[0]; + var volume = args[1]; + var thisM = Media.get(id); + thisM.volume = volume; + } +}; + +require("cordova/exec/proxy").add("Media",module.exports); diff --git a/plugins/org.apache.cordova.media/src/wp/AudioPlayer.cs b/plugins/org.apache.cordova.media/src/wp/AudioPlayer.cs new file mode 100644 index 00000000..882eb96e --- /dev/null +++ b/plugins/org.apache.cordova.media/src/wp/AudioPlayer.cs @@ -0,0 +1,647 @@ +/* + Licensed 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. +*/ + +using System; +using System.IO; +using System.IO.IsolatedStorage; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Threading; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Media; +using Microsoft.Phone.Controls; +using System.Diagnostics; +using System.Windows.Resources; + +namespace WPCordovaClassLib.Cordova.Commands +{ + + /// <summary> + /// Implements audio record and play back functionality. + /// </summary> + internal class AudioPlayer : IDisposable + { + #region Constants + + // AudioPlayer states + private const int PlayerState_None = 0; + private const int PlayerState_Starting = 1; + private const int PlayerState_Running = 2; + private const int PlayerState_Paused = 3; + private const int PlayerState_Stopped = 4; + + // AudioPlayer messages + private const int MediaState = 1; + private const int MediaDuration = 2; + private const int MediaPosition = 3; + private const int MediaError = 9; + + // AudioPlayer errors + private const int MediaErrorPlayModeSet = 1; + private const int MediaErrorAlreadyRecording = 2; + private const int MediaErrorStartingRecording = 3; + private const int MediaErrorRecordModeSet = 4; + private const int MediaErrorStartingPlayback = 5; + private const int MediaErrorResumeState = 6; + private const int MediaErrorPauseState = 7; + private const int MediaErrorStopState = 8; + + //TODO: get rid of this callback, it should be universal + //private const string CallbackFunction = "CordovaMediaonStatus"; + + #endregion + + + /// <summary> + /// The AudioHandler object + /// </summary> + private Media handler; + + /// <summary> + /// Temporary buffer to store audio chunk + /// </summary> + private byte[] buffer; + + /// <summary> + /// Xna game loop dispatcher + /// </summary> + DispatcherTimer dtXna; + + + /// <summary> + /// Output buffer + /// </summary> + private MemoryStream memoryStream; + + /// <summary> + /// The id of this player (used to identify Media object in JavaScript) + /// </summary> + private String id; + + /// <summary> + /// State of recording or playback + /// </summary> + private int state = PlayerState_None; + + /// <summary> + /// File name to play or record to + /// </summary> + private String audioFile = null; + + /// <summary> + /// Duration of audio + /// </summary> + private double duration = -1; + + /// <summary> + /// Audio player object + /// </summary> + private MediaElement player = null; + + /// <summary> + /// Audio source + /// </summary> + private Microphone recorder; + + /// <summary> + /// Internal flag specified that we should only open audio w/o playing it + /// </summary> + private bool prepareOnly = false; + + /// <summary> + /// Creates AudioPlayer instance + /// </summary> + /// <param name="handler">Media object</param> + /// <param name="id">player id</param> + public AudioPlayer(Media handler, String id) + { + this.handler = handler; + this.id = id; + } + + + /// <summary> + /// Destroys player and stop audio playing or recording + /// </summary> + public void Dispose() + { + if (this.player != null) + { + this.stopPlaying(); + this.player = null; + } + if (this.recorder != null) + { + this.stopRecording(); + this.recorder = null; + } + + this.FinalizeXnaGameLoop(); + } + + private void InvokeCallback(int message, string value, bool removeHandler) + { + string args = string.Format("('{0}',{1},{2});", this.id, message, value); + string callback = @"(function(id,msg,value){ + try { + if (msg == Media.MEDIA_ERROR) { + value = {'code':value}; + } + Media.onStatus(id,msg,value); + } + catch(e) { + console.log('Error calling Media.onStatus :: ' + e); + } + })" + args; + this.handler.InvokeCustomScript(new ScriptCallback("eval", new string[] { callback }), false); + } + + private void InvokeCallback(int message, int value, bool removeHandler) + { + InvokeCallback(message, value.ToString(), removeHandler); + } + + private void InvokeCallback(int message, double value, bool removeHandler) + { + InvokeCallback(message, value.ToString(), removeHandler); + } + + /// <summary> + /// Starts recording, data is stored in memory + /// </summary> + /// <param name="filePath"></param> + public void startRecording(string filePath) + { + if (this.player != null) + { + InvokeCallback(MediaError, MediaErrorPlayModeSet, false); + } + else if (this.recorder == null) + { + try + { + this.audioFile = filePath; + this.InitializeXnaGameLoop(); + this.recorder = Microphone.Default; + this.recorder.BufferDuration = TimeSpan.FromMilliseconds(500); + this.buffer = new byte[recorder.GetSampleSizeInBytes(this.recorder.BufferDuration)]; + this.recorder.BufferReady += new EventHandler<EventArgs>(recorderBufferReady); + MemoryStream stream = new MemoryStream(); + this.memoryStream = stream; + int numBits = 16; + int numBytes = numBits / 8; + + // inline version from AudioFormatsHelper + stream.Write(System.Text.Encoding.UTF8.GetBytes("RIFF"), 0, 4); + stream.Write(BitConverter.GetBytes(0), 0, 4); + stream.Write(System.Text.Encoding.UTF8.GetBytes("WAVE"), 0, 4); + stream.Write(System.Text.Encoding.UTF8.GetBytes("fmt "), 0, 4); + stream.Write(BitConverter.GetBytes(16), 0, 4); + stream.Write(BitConverter.GetBytes((short)1), 0, 2); + stream.Write(BitConverter.GetBytes((short)1), 0, 2); + stream.Write(BitConverter.GetBytes(this.recorder.SampleRate), 0, 4); + stream.Write(BitConverter.GetBytes(this.recorder.SampleRate * numBytes), 0, 4); + stream.Write(BitConverter.GetBytes((short)(numBytes)), 0, 2); + stream.Write(BitConverter.GetBytes((short)(numBits)), 0, 2); + stream.Write(System.Text.Encoding.UTF8.GetBytes("data"), 0, 4); + stream.Write(BitConverter.GetBytes(0), 0, 4); + + this.recorder.Start(); + FrameworkDispatcher.Update(); + this.SetState(PlayerState_Running); + } + catch (Exception) + { + InvokeCallback(MediaError, MediaErrorStartingRecording, false); + //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorStartingRecording),false); + } + } + else + { + InvokeCallback(MediaError, MediaErrorAlreadyRecording, false); + //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorAlreadyRecording),false); + } + } + + /// <summary> + /// Stops recording + /// </summary> + public void stopRecording() + { + if (this.recorder != null) + { + if (this.state == PlayerState_Running) + { + try + { + this.recorder.Stop(); + this.recorder.BufferReady -= recorderBufferReady; + this.recorder = null; + SaveAudioClipToLocalStorage(); + this.FinalizeXnaGameLoop(); + this.SetState(PlayerState_Stopped); + } + catch (Exception) + { + //TODO + } + } + } + } + + /// <summary> + /// Starts or resume playing audio file + /// </summary> + /// <param name="filePath">The name of the audio file</param> + /// <summary> + /// Starts or resume playing audio file + /// </summary> + /// <param name="filePath">The name of the audio file</param> + public void startPlaying(string filePath) + { + if (this.recorder != null) + { + InvokeCallback(MediaError, MediaErrorRecordModeSet, false); + //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorRecordModeSet),false); + return; + } + + + if (this.player == null || this.player.Source.AbsolutePath.LastIndexOf(filePath) < 0) + { + try + { + // this.player is a MediaElement, it must be added to the visual tree in order to play + PhoneApplicationFrame frame = Application.Current.RootVisual as PhoneApplicationFrame; + if (frame != null) + { + PhoneApplicationPage page = frame.Content as PhoneApplicationPage; + if (page != null) + { + Grid grid = page.FindName("LayoutRoot") as Grid; + if (grid != null) + { + + this.player = grid.FindName("playerMediaElement") as MediaElement; + if (this.player == null) // still null ? + { + this.player = new MediaElement(); + this.player.Name = "playerMediaElement"; + grid.Children.Add(this.player); + this.player.Visibility = Visibility.Visible; + } + if (this.player.CurrentState == System.Windows.Media.MediaElementState.Playing) + { + this.player.Stop(); // stop it! + } + + this.player.Source = null; // Garbage collect it. + this.player.MediaOpened += MediaOpened; + this.player.MediaEnded += MediaEnded; + this.player.MediaFailed += MediaFailed; + } + } + } + + this.audioFile = filePath; + + Uri uri = new Uri(filePath, UriKind.RelativeOrAbsolute); + if (uri.IsAbsoluteUri) + { + this.player.Source = uri; + } + else + { + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + if (!isoFile.FileExists(filePath)) + { + // try to unpack it from the dll into isolated storage + StreamResourceInfo fileResourceStreamInfo = Application.GetResourceStream(new Uri(filePath, UriKind.Relative)); + if (fileResourceStreamInfo != null) + { + using (BinaryReader br = new BinaryReader(fileResourceStreamInfo.Stream)) + { + byte[] data = br.ReadBytes((int)fileResourceStreamInfo.Stream.Length); + + string[] dirParts = filePath.Split('/'); + string dirName = ""; + for (int n = 0; n < dirParts.Length - 1; n++) + { + dirName += dirParts[n] + "/"; + } + if (!isoFile.DirectoryExists(dirName)) + { + isoFile.CreateDirectory(dirName); + } + + using (IsolatedStorageFileStream outFile = isoFile.OpenFile(filePath, FileMode.Create)) + { + using (BinaryWriter writer = new BinaryWriter(outFile)) + { + writer.Write(data); + } + } + } + } + } + if (isoFile.FileExists(filePath)) + { + using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream(filePath, FileMode.Open, isoFile)) + { + this.player.SetSource(stream); + } + } + else + { + InvokeCallback(MediaError, MediaErrorPlayModeSet, false); + //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, 1), false); + return; + } + } + } + this.SetState(PlayerState_Starting); + } + catch (Exception e) + { + Debug.WriteLine("Error in AudioPlayer::startPlaying : " + e.Message); + InvokeCallback(MediaError, MediaErrorStartingPlayback, false); + //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorStartingPlayback),false); + } + } + else + { + if (this.state != PlayerState_Running) + { + this.player.Play(); + this.SetState(PlayerState_Running); + } + else + { + InvokeCallback(MediaError, MediaErrorResumeState, false); + //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorResumeState),false); + } + } + } + + /// <summary> + /// Callback to be invoked when the media source is ready for playback + /// </summary> + private void MediaOpened(object sender, RoutedEventArgs arg) + { + if (this.player != null) + { + this.duration = this.player.NaturalDuration.TimeSpan.TotalSeconds; + InvokeCallback(MediaDuration, this.duration, false); + //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaDuration, this.duration),false); + if (!this.prepareOnly) + { + this.player.Play(); + this.SetState(PlayerState_Running); + } + this.prepareOnly = false; + } + else + { + // TODO: occasionally MediaOpened is signalled, but player is null + } + } + + /// <summary> + /// Callback to be invoked when playback of a media source has completed + /// </summary> + private void MediaEnded(object sender, RoutedEventArgs arg) + { + this.SetState(PlayerState_Stopped); + } + + /// <summary> + /// Callback to be invoked when playback of a media source has failed + /// </summary> + private void MediaFailed(object sender, RoutedEventArgs arg) + { + player.Stop(); + InvokeCallback(MediaError, MediaErrorStartingPlayback, false); + //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError.ToString(), "Media failed"),false); + } + + /// <summary> + /// Seek or jump to a new time in the track + /// </summary> + /// <param name="milliseconds">The new track position</param> + public void seekToPlaying(int milliseconds) + { + if (this.player != null) + { + TimeSpan tsPos = new TimeSpan(0, 0, 0, 0, milliseconds); + this.player.Position = tsPos; + InvokeCallback(MediaPosition, milliseconds / 1000.0f, false); + //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaPosition, milliseconds / 1000.0f),false); + } + } + + /// <summary> + /// Set the volume of the player + /// </summary> + /// <param name="vol">volume 0.0-1.0, default value is 0.5</param> + public void setVolume(double vol) + { + if (this.player != null) + { + this.player.Volume = vol; + } + } + + /// <summary> + /// Pauses playing + /// </summary> + public void pausePlaying() + { + if (this.state == PlayerState_Running) + { + this.player.Pause(); + this.SetState(PlayerState_Paused); + } + else + { + InvokeCallback(MediaError, MediaErrorPauseState, false); + //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorPauseState),false); + } + } + + + /// <summary> + /// Stops playing the audio file + /// </summary> + public void stopPlaying() + { + if ((this.state == PlayerState_Running) || (this.state == PlayerState_Paused)) + { + this.player.Stop(); + + this.player.Position = new TimeSpan(0L); + this.SetState(PlayerState_Stopped); + } + //else // Why is it an error to call stop on a stopped media? + //{ + // this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorStopState), false); + //} + } + + /// <summary> + /// Gets current position of playback + /// </summary> + /// <returns>current position</returns> + public double getCurrentPosition() + { + if ((this.state == PlayerState_Running) || (this.state == PlayerState_Paused)) + { + double currentPosition = this.player.Position.TotalSeconds; + //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaPosition, currentPosition),false); + return currentPosition; + } + else + { + return 0; + } + } + + /// <summary> + /// Gets the duration of the audio file + /// </summary> + /// <param name="filePath">The name of the audio file</param> + /// <returns>track duration</returns> + public double getDuration(string filePath) + { + if (this.recorder != null) + { + return (-2); + } + + if (this.player != null) + { + return this.duration; + + } + else + { + this.prepareOnly = true; + this.startPlaying(filePath); + return this.duration; + } + } + + /// <summary> + /// Sets the state and send it to JavaScript + /// </summary> + /// <param name="state">state</param> + private void SetState(int state) + { + if (this.state != state) + { + InvokeCallback(MediaState, state, false); + //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaState, state),false); + } + + this.state = state; + } + + #region record methods + + /// <summary> + /// Copies data from recorder to memory storages and updates recording state + /// </summary> + /// <param name="sender"></param> + /// <param name="e"></param> + private void recorderBufferReady(object sender, EventArgs e) + { + this.recorder.GetData(this.buffer); + this.memoryStream.Write(this.buffer, 0, this.buffer.Length); + } + + /// <summary> + /// Writes audio data from memory to isolated storage + /// </summary> + /// <returns></returns> + private void SaveAudioClipToLocalStorage() + { + if (memoryStream == null || memoryStream.Length <= 0) + { + return; + } + + long position = memoryStream.Position; + memoryStream.Seek(4, SeekOrigin.Begin); + memoryStream.Write(BitConverter.GetBytes((int)memoryStream.Length - 8), 0, 4); + memoryStream.Seek(40, SeekOrigin.Begin); + memoryStream.Write(BitConverter.GetBytes((int)memoryStream.Length - 44), 0, 4); + memoryStream.Seek(position, SeekOrigin.Begin); + + try + { + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + string directory = Path.GetDirectoryName(audioFile); + + if (!isoFile.DirectoryExists(directory)) + { + isoFile.CreateDirectory(directory); + } + + this.memoryStream.Seek(0, SeekOrigin.Begin); + + using (IsolatedStorageFileStream fileStream = isoFile.CreateFile(audioFile)) + { + this.memoryStream.CopyTo(fileStream); + } + } + } + catch (Exception) + { + //TODO: log or do something else + throw; + } + } + + #region Xna loop + /// <summary> + /// Special initialization required for the microphone: XNA game loop + /// </summary> + private void InitializeXnaGameLoop() + { + // Timer to simulate the XNA game loop (Microphone is from XNA) + this.dtXna = new DispatcherTimer(); + this.dtXna.Interval = TimeSpan.FromMilliseconds(33); + this.dtXna.Tick += delegate { try { FrameworkDispatcher.Update(); } catch { } }; + this.dtXna.Start(); + } + /// <summary> + /// Finalizes XNA game loop for microphone + /// </summary> + private void FinalizeXnaGameLoop() + { + // Timer to simulate the XNA game loop (Microphone is from XNA) + if (this.dtXna != null) + { + this.dtXna.Stop(); + this.dtXna = null; + } + } + + #endregion + + #endregion + } +} diff --git a/plugins/org.apache.cordova.media/src/wp/Media.cs b/plugins/org.apache.cordova.media/src/wp/Media.cs new file mode 100644 index 00000000..aedd2bb6 --- /dev/null +++ b/plugins/org.apache.cordova.media/src/wp/Media.cs @@ -0,0 +1,590 @@ +/* + Licensed 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. +*/ + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Windows; +using System.Diagnostics; + +namespace WPCordovaClassLib.Cordova.Commands +{ + /// <summary> + /// Provides the ability to record and play back audio files on a device. + /// </summary> + public class Media : BaseCommand + { + /// <summary> + /// Audio player objects + /// </summary> + private static Dictionary<string, AudioPlayer> players = new Dictionary<string, AudioPlayer>(); + + /// <summary> + /// Represents Media action options. + /// </summary> + [DataContract] + public class MediaOptions + { + /// <summary> + /// Audio id + /// </summary> + [DataMember(Name = "id", IsRequired = true)] + public string Id { get; set; } + + /// <summary> + /// Path to audio file + /// </summary> + [DataMember(Name = "src")] + public string Src { get; set; } + + /// <summary> + /// New track position + /// </summary> + [DataMember(Name = "milliseconds")] + public int Milliseconds { get; set; } + + public string CallbackId { get; set; } + } + + /// <summary> + /// Releases the audio player instance to save memory. + /// </summary> + public void release(string options) + { + string callbackId = this.CurrentCommandCallbackId; + try + { + MediaOptions mediaOptions = new MediaOptions(); + + try + { + string[] optionsString = JSON.JsonHelper.Deserialize<string[]>(options); + mediaOptions.Id = optionsString[0]; + callbackId = mediaOptions.CallbackId = optionsString[1]; + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION), callbackId); + return; + } + + if (!Media.players.ContainsKey(mediaOptions.Id)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, false), callbackId); + return; + } + + Deployment.Current.Dispatcher.BeginInvoke(() => + { + try + { + AudioPlayer audio = Media.players[mediaOptions.Id]; + Media.players.Remove(mediaOptions.Id); + audio.Dispose(); + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, true), mediaOptions.CallbackId); + } + catch (Exception e) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message), mediaOptions.CallbackId); + } + }); + } + catch (Exception e) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message), callbackId); + } + } + + private AudioPlayer GetOrCreatePlayerById(string id) + { + AudioPlayer audio = null; + + lock (Media.players) + { + if (!Media.players.TryGetValue(id, out audio)) + { + audio = new AudioPlayer(this, id); + Media.players.Add(id, audio); + Debug.WriteLine("Media Created in GetOrCreatePlayerById"); + } + } + + + + return audio; + } + + /// <summary> + /// Starts recording and save the specified file + /// </summary> + public void startRecordingAudio(string options) + { + string callbackId = this.CurrentCommandCallbackId; + try + { + MediaOptions mediaOptions = new MediaOptions(); + + try + { + string[] optionsString = JSON.JsonHelper.Deserialize<string[]>(options); + mediaOptions.Id = optionsString[0]; + mediaOptions.Src = optionsString[1]; + callbackId = mediaOptions.CallbackId = optionsString[2]; + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION), mediaOptions.CallbackId); + return; + } + + if (mediaOptions != null) + { + + Deployment.Current.Dispatcher.BeginInvoke(() => + { + try + { + AudioPlayer audio; + if (!Media.players.ContainsKey(mediaOptions.Id)) + { + audio = new AudioPlayer(this, mediaOptions.Id); + Media.players.Add(mediaOptions.Id, audio); + } + else + { + audio = Media.players[mediaOptions.Id]; + } + + if (audio != null) + { + audio.startRecording(mediaOptions.Src); + DispatchCommandResult(new PluginResult(PluginResult.Status.OK), mediaOptions.CallbackId); + } + else + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, + "Error accessing AudioPlayer for key " + mediaOptions.Id), mediaOptions.CallbackId); + } + + + } + catch (Exception e) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message), mediaOptions.CallbackId); + } + + }); + } + else + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION), mediaOptions.CallbackId); + } + } + catch (Exception e) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message), callbackId); + } + } + + /// <summary> + /// Stops recording and save to the file specified when recording started + /// </summary> + public void stopRecordingAudio(string options) + { + string callbackId = this.CurrentCommandCallbackId; + + try + { + string[] optStrings = JSON.JsonHelper.Deserialize<string[]>(options); + string mediaId = optStrings[0]; + callbackId = optStrings[1]; + Deployment.Current.Dispatcher.BeginInvoke(() => + { + try + { + if (Media.players.ContainsKey(mediaId)) + { + AudioPlayer audio = Media.players[mediaId]; + audio.stopRecording(); + Media.players.Remove(mediaId); + } + DispatchCommandResult(new PluginResult(PluginResult.Status.OK), callbackId); + } + catch (Exception e) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message), callbackId); + } + }); + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION), callbackId); + } + } + + public void setVolume(string options) // id,volume + { + string callbackId = this.CurrentCommandCallbackId; + try + { + string[] optionsString = JSON.JsonHelper.Deserialize<string[]>(options); + string id = optionsString[0]; + double volume = 0.0d; + double.TryParse(optionsString[1], out volume); + + callbackId = optionsString[2]; + + if (Media.players.ContainsKey(id)) + { + Deployment.Current.Dispatcher.BeginInvoke(() => + { + try + { + AudioPlayer player = Media.players[id]; + player.setVolume(volume); + } + catch (Exception e) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message), callbackId); + } + }); + } + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, + "Error parsing options into setVolume method"), callbackId); + } + } + + // Some Audio Notes: + // In the Windows Phone Emulator, playback of video or audio content using the MediaElement control is not supported. + // While playing, a MediaElement stops all other media playback on the phone. + // Multiple MediaElement controls are NOT supported + + // Called when you create a new Media('blah.wav') object in JS. + public void create(string options) + { + string callbackId = this.CurrentCommandCallbackId; + try + { + MediaOptions mediaOptions; + try + { + string[] optionsString = JSON.JsonHelper.Deserialize<string[]>(options); + mediaOptions = new MediaOptions(); + mediaOptions.Id = optionsString[0]; + mediaOptions.Src = optionsString[1]; + callbackId = mediaOptions.CallbackId = optionsString[2]; + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, + "Error parsing options into create method"), callbackId); + return; + } + + GetOrCreatePlayerById(mediaOptions.Id); + DispatchCommandResult(new PluginResult(PluginResult.Status.OK), callbackId); + + } + catch (Exception e) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message), callbackId); + } + } + + /// <summary> + /// Starts or resume playing audio file + /// </summary> + public void startPlayingAudio(string options) + { + string callbackId = this.CurrentCommandCallbackId; + try + { + MediaOptions mediaOptions; + try + { + string[] optionsString = JSON.JsonHelper.Deserialize<string[]>(options); + mediaOptions = new MediaOptions(); + mediaOptions.Id = optionsString[0]; + mediaOptions.Src = optionsString[1]; + int msec = 0; + if (int.TryParse(optionsString[2], out msec)) + { + mediaOptions.Milliseconds = msec; + } + callbackId = mediaOptions.CallbackId = optionsString[3]; + + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION), callbackId); + return; + } + + AudioPlayer audio = GetOrCreatePlayerById(mediaOptions.Id); + + Deployment.Current.Dispatcher.BeginInvoke(() => + { + try + { + audio.startPlaying(mediaOptions.Src); + DispatchCommandResult(new PluginResult(PluginResult.Status.OK), callbackId); + } + catch (Exception e) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message), callbackId); + } + }); + } + catch (Exception e) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message), callbackId); + } + } + + + /// <summary> + /// Seeks to a location + /// </summary> + public void seekToAudio(string options) + { + string callbackId = this.CurrentCommandCallbackId; + try + { + MediaOptions mediaOptions; + + try + { + string[] optionsString = JSON.JsonHelper.Deserialize<string[]>(options); + mediaOptions = new MediaOptions(); + mediaOptions.Id = optionsString[0]; + int msec = 0; + if (int.TryParse(optionsString[2], out msec)) + { + mediaOptions.Milliseconds = msec; + } + callbackId = mediaOptions.CallbackId = optionsString[3]; + + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION), callbackId); + return; + } + + Deployment.Current.Dispatcher.BeginInvoke(() => + { + try + { + if (Media.players.ContainsKey(mediaOptions.Id)) + { + AudioPlayer audio = Media.players[mediaOptions.Id]; + audio.seekToPlaying(mediaOptions.Milliseconds); + } + else + { + Debug.WriteLine("ERROR: seekToAudio could not find mediaPlayer for " + mediaOptions.Id); + } + + DispatchCommandResult(new PluginResult(PluginResult.Status.OK), callbackId); + } + catch (Exception e) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message), callbackId); + } + }); + } + catch (Exception e) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message), callbackId); + } + } + + /// <summary> + /// Pauses playing + /// </summary> + public void pausePlayingAudio(string options) + { + string callbackId = this.CurrentCommandCallbackId; + try + { + string[] optionsString = JSON.JsonHelper.Deserialize<string[]>(options); + string mediaId = optionsString[0]; + callbackId = optionsString[1]; + + Deployment.Current.Dispatcher.BeginInvoke(() => + { + try + { + if (Media.players.ContainsKey(mediaId)) + { + AudioPlayer audio = Media.players[mediaId]; + audio.pausePlaying(); + } + else + { + Debug.WriteLine("ERROR: pausePlayingAudio could not find mediaPlayer for " + mediaId); + } + + DispatchCommandResult(new PluginResult(PluginResult.Status.OK), callbackId); + } + catch (Exception e) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message),callbackId); + } + }); + + + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION),callbackId); + } + + + } + + + /// <summary> + /// Stops playing the audio file + /// </summary> + public void stopPlayingAudio(String options) + { + string callbackId = this.CurrentCommandCallbackId; + try + { + string[] optionsStrings = JSON.JsonHelper.Deserialize<string[]>(options); + string mediaId = optionsStrings[0]; + callbackId = optionsStrings[1]; + Deployment.Current.Dispatcher.BeginInvoke(() => + { + try + { + if (Media.players.ContainsKey(mediaId)) + { + AudioPlayer audio = Media.players[mediaId]; + audio.stopPlaying(); + } + else + { + Debug.WriteLine("stopPlaying could not find mediaPlayer for " + mediaId); + } + + DispatchCommandResult(new PluginResult(PluginResult.Status.OK), callbackId); + } + catch (Exception e) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message), callbackId); + } + }); + + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION), callbackId); + } + } + + /// <summary> + /// Gets current position of playback + /// </summary> + public void getCurrentPositionAudio(string options) + { + string callbackId = this.CurrentCommandCallbackId; + try + { + string[] optionsStrings = JSON.JsonHelper.Deserialize<string[]>(options); + string mediaId = optionsStrings[0]; + callbackId = optionsStrings[1]; + Deployment.Current.Dispatcher.BeginInvoke(() => + { + try + { + if (Media.players.ContainsKey(mediaId)) + { + AudioPlayer audio = Media.players[mediaId]; + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, audio.getCurrentPosition()), callbackId); + } + else + { + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, -1), callbackId); + } + } + catch (Exception e) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message), callbackId); + } + }); + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION), callbackId); + return; + } + } + + + /// <summary> + /// Gets the duration of the audio file + /// </summary> + + [Obsolete("This method will be removed shortly")] + public void getDurationAudio(string options) + { + string callbackId = this.CurrentCommandCallbackId; + try + { + MediaOptions mediaOptions; + + try + { + string[] optionsString = JSON.JsonHelper.Deserialize<string[]>(options); + + mediaOptions = new MediaOptions(); + mediaOptions.Id = optionsString[0]; + mediaOptions.Src = optionsString[1]; + callbackId = mediaOptions.CallbackId = optionsString[2]; + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION), callbackId); + return; + } + + AudioPlayer audio; + if (Media.players.ContainsKey(mediaOptions.Id)) + { + audio = Media.players[mediaOptions.Id]; + } + else + { + Debug.WriteLine("ERROR: getDurationAudio could not find mediaPlayer for " + mediaOptions.Id); + audio = new AudioPlayer(this, mediaOptions.Id); + Media.players.Add(mediaOptions.Id, audio); + } + + Deployment.Current.Dispatcher.BeginInvoke(() => + { + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, audio.getDuration(mediaOptions.Src)), callbackId); + }); + } + catch (Exception e) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, e.Message), callbackId); + } + } + } +} |
