You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by ti...@apache.org on 2013/06/18 02:11:37 UTC

[3/3] git commit: Needed to include this file

Needed to include this file


Project: http://git-wip-us.apache.org/repos/asf/cordova-plugin-media/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-plugin-media/commit/a6be9efd
Tree: http://git-wip-us.apache.org/repos/asf/cordova-plugin-media/tree/a6be9efd
Diff: http://git-wip-us.apache.org/repos/asf/cordova-plugin-media/diff/a6be9efd

Branch: refs/heads/master
Commit: a6be9efd168dc34cb8592441d847f05251241b94
Parents: 27ccee1
Author: Tim Kim <ti...@adobe.com>
Authored: Mon Jun 17 17:11:31 2013 -0700
Committer: Tim Kim <ti...@adobe.com>
Committed: Mon Jun 17 17:11:31 2013 -0700

----------------------------------------------------------------------
 src/android/AudioPlayer.java | 553 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 553 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-plugin-media/blob/a6be9efd/src/android/AudioPlayer.java
----------------------------------------------------------------------
diff --git a/src/android/AudioPlayer.java b/src/android/AudioPlayer.java
new file mode 100644
index 0000000..271dfa3
--- /dev/null
+++ b/src/android/AudioPlayer.java
@@ -0,0 +1,553 @@
+/*
+       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.core;
+
+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 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.");
+            this.handler.webView.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', "+MEDIA_ERROR+", { \"code\":"+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();
+            }
+            this.handler.webView.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', "+MEDIA_ERROR+", { \"code\":"+MEDIA_ERR_ABORTED+"});");
+            break;
+        case RECORD:
+            Log.d(LOG_TAG, "AudioPlayer Error: Already recording.");
+            this.handler.webView.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', "+MEDIA_ERROR+", { \"code\":"+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");
+            this.handler.webView.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_POSITION + ", " + 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());
+            this.handler.webView.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + 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());
+            this.handler.webView.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + 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();
+            this.handler.webView.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_POSITION + ", " + 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
+        this.handler.webView.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_DURATION + "," + 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
+        this.handler.webView.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', { \"code\":" + arg1 + "});");
+        return false;
+    }
+
+    /**
+     * Set the state and send it to JavaScript.
+     *
+     * @param state
+     */
+    private void setState(STATE state) {
+        if (this.state != state) {
+            this.handler.webView.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_STATE + ", " + state.ordinal() + ");");
+        }
+        this.state = state;
+    }
+
+    /**
+     * Set the mode and send it to JavaScript.
+     *
+     * @param state
+     */
+    private void setMode(MODE mode) {
+        if (this.mode != mode) {
+            //mode is not part of the expected behavior, so no notification
+            //this.handler.webView.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_STATE + ", " + mode + ");");
+        }
+        this.mode = mode;
+    }
+
+    /**
+     * Get the audio state.
+     *
+     * @return int
+     */
+    public int getState() {
+        return this.state.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.");
+            this.handler.webView.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});");
+            return false; //player is not ready
+        }
+        return true;
+    }
+
+    /**
+     * attempts to initialize the media player for playback
+     * @param file the file to play
+     * @return false if player not ready, reports if in wrong mode or state
+     */
+    private boolean readyPlayer(String file) {
+        if (playMode()) {
+            switch (this.state) {
+                case MEDIA_NONE:
+                    if (this.player == null) {
+                        this.player = new MediaPlayer();
+                    }
+                    try {
+                        this.loadAudioFile(file);
+                    } catch (Exception e) {
+                        this.handler.webView.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', "+MEDIA_ERROR+", { \"code\":"+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) {
+                            this.handler.webView.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});");
+                        }
+                        //if we had to prepare= the file, we won't be in the correct state for playback
+                        return false;
+                    }
+                default:
+                    Log.d(LOG_TAG, "AudioPlayer Error: startPlaying() called during invalid state: " + this.state);
+                    this.handler.webView.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});");
+            }
+        }
+        return false;
+    }
+
+    /**
+     * load audio file
+     * @throws IOException
+     * @throws IllegalStateException
+     * @throws SecurityException
+     * @throws IllegalArgumentException
+     */
+    private void loadAudioFile(String file) throws IllegalArgumentException, SecurityException, IllegalStateException, IOException {
+        if (this.isStreaming(file)) {
+            this.player.setDataSource(file);
+            this.player.setAudioStreamType(AudioManager.STREAM_MUSIC);
+            //if it's a streaming file, play mode is implied
+            this.setMode(MODE.PLAY);
+            this.setState(STATE.MEDIA_STARTING);
+            this.player.setOnPreparedListener(this);
+            this.player.prepareAsync();
+        }
+        else {
+            if (file.startsWith("/android_asset/")) {
+                String f = file.substring(15);
+                android.content.res.AssetFileDescriptor fd = this.handler.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());
+                }
+                else {
+                    this.player.setDataSource("/sdcard/" + file);
+                }
+            }
+                this.setState(STATE.MEDIA_STARTING);
+                this.player.setOnPreparedListener(this);
+                this.player.prepare();
+
+                // Get duration
+                this.duration = getDurationInSeconds();
+            }
+    }
+}