Home | History | Annotate | Download | only in variablespeed
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ex.variablespeed;
     18 
     19 import com.google.common.base.Preconditions;
     20 
     21 import android.content.Context;
     22 import android.media.MediaPlayer;
     23 import android.net.Uri;
     24 import android.util.Log;
     25 
     26 import java.io.IOException;
     27 import java.util.concurrent.CountDownLatch;
     28 import java.util.concurrent.Executor;
     29 import java.util.concurrent.TimeUnit;
     30 import java.util.concurrent.TimeoutException;
     31 
     32 import javax.annotation.concurrent.GuardedBy;
     33 import javax.annotation.concurrent.ThreadSafe;
     34 
     35 /**
     36  * This class behaves in a similar fashion to the MediaPlayer, but by using
     37  * native code it is able to use variable-speed playback.
     38  * <p>
     39  * This class is thread-safe. It's not yet perfect though, see the unit tests
     40  * for details - there is insufficient testing for the concurrent logic. You are
     41  * probably best advised to use thread confinment until the unit tests are more
     42  * complete with regards to threading.
     43  * <p>
     44  * The easiest way to ensure that calls to this class are not made concurrently
     45  * (besides only ever accessing it from one thread) is to wrap it in a
     46  * {@link SingleThreadedMediaPlayerProxy}, designed just for this purpose.
     47  */
     48 @ThreadSafe
     49 public class VariableSpeed implements MediaPlayerProxy {
     50     private static final String TAG = "VariableSpeed";
     51 
     52     private final Executor mExecutor;
     53     private final Object lock = new Object();
     54     @GuardedBy("lock") private MediaPlayerDataSource mDataSource;
     55     @GuardedBy("lock") private boolean mIsPrepared;
     56     @GuardedBy("lock") private boolean mHasDuration;
     57     @GuardedBy("lock") private boolean mHasStartedPlayback;
     58     @GuardedBy("lock") private CountDownLatch mEngineInitializedLatch;
     59     @GuardedBy("lock") private CountDownLatch mPlaybackFinishedLatch;
     60     @GuardedBy("lock") private boolean mHasBeenReleased = true;
     61     @GuardedBy("lock") private boolean mIsReadyToReUse = true;
     62     @GuardedBy("lock") private boolean mSkipCompletionReport;
     63     @GuardedBy("lock") private int mStartPosition;
     64     @GuardedBy("lock") private float mCurrentPlaybackRate = 1.0f;
     65     @GuardedBy("lock") private int mDuration;
     66     @GuardedBy("lock") private MediaPlayer.OnCompletionListener mCompletionListener;
     67     @GuardedBy("lock") private int mAudioStreamType;
     68 
     69     private VariableSpeed(Executor executor) throws UnsupportedOperationException {
     70         Preconditions.checkNotNull(executor);
     71         mExecutor = executor;
     72         try {
     73             VariableSpeedNative.loadLibrary();
     74         } catch (UnsatisfiedLinkError e) {
     75             throw new UnsupportedOperationException("could not load library", e);
     76         } catch (SecurityException e) {
     77             throw new UnsupportedOperationException("could not load library", e);
     78         }
     79         reset();
     80     }
     81 
     82     public static MediaPlayerProxy createVariableSpeed(Executor executor)
     83             throws UnsupportedOperationException {
     84         return new SingleThreadedMediaPlayerProxy(new VariableSpeed(executor));
     85     }
     86 
     87     @Override
     88     public void setOnCompletionListener(MediaPlayer.OnCompletionListener listener) {
     89         synchronized (lock) {
     90             check(!mHasBeenReleased, "has been released, reset before use");
     91             mCompletionListener = listener;
     92         }
     93     }
     94 
     95     @Override
     96     public void setOnErrorListener(MediaPlayer.OnErrorListener listener) {
     97         synchronized (lock) {
     98             check(!mHasBeenReleased, "has been released, reset before use");
     99             // TODO: I haven't actually added any error listener code.
    100         }
    101     }
    102 
    103     @Override
    104     public void release() {
    105         synchronized (lock) {
    106             if (mHasBeenReleased) {
    107                 return;
    108             }
    109             mHasBeenReleased = true;
    110         }
    111         stopCurrentPlayback();
    112         boolean requiresShutdown = false;
    113         synchronized (lock) {
    114             requiresShutdown = hasEngineBeenInitialized();
    115         }
    116         if (requiresShutdown) {
    117             VariableSpeedNative.shutdownEngine();
    118         }
    119         synchronized (lock) {
    120             mIsReadyToReUse = true;
    121         }
    122     }
    123 
    124     private boolean hasEngineBeenInitialized() {
    125         return mEngineInitializedLatch.getCount() <= 0;
    126     }
    127 
    128     private boolean hasPlaybackFinished() {
    129         return mPlaybackFinishedLatch.getCount() <= 0;
    130     }
    131 
    132     /**
    133      * Stops the current playback, returns once it has stopped.
    134      */
    135     private void stopCurrentPlayback() {
    136         boolean isPlaying;
    137         CountDownLatch engineInitializedLatch;
    138         CountDownLatch playbackFinishedLatch;
    139         synchronized (lock) {
    140             isPlaying = mHasStartedPlayback && !hasPlaybackFinished();
    141             engineInitializedLatch = mEngineInitializedLatch;
    142             playbackFinishedLatch = mPlaybackFinishedLatch;
    143             if (isPlaying) {
    144                 mSkipCompletionReport = true;
    145             }
    146         }
    147         if (isPlaying) {
    148             waitForLatch(engineInitializedLatch);
    149             VariableSpeedNative.stopPlayback();
    150             waitForLatch(playbackFinishedLatch);
    151         }
    152     }
    153 
    154     private void waitForLatch(CountDownLatch latch) {
    155         try {
    156             boolean success = latch.await(1, TimeUnit.SECONDS);
    157             if (!success) {
    158                 reportException(new TimeoutException("waited too long"));
    159             }
    160         } catch (InterruptedException e) {
    161             // Preserve the interrupt status, though this is unexpected.
    162             Thread.currentThread().interrupt();
    163             reportException(e);
    164         }
    165     }
    166 
    167     @Override
    168     public void setDataSource(Context context, Uri intentUri) {
    169         checkNotNull(context, "context");
    170         checkNotNull(intentUri, "intentUri");
    171         innerSetDataSource(new MediaPlayerDataSource(context, intentUri));
    172     }
    173 
    174     @Override
    175     public void setDataSource(String path) {
    176         checkNotNull(path, "path");
    177         innerSetDataSource(new MediaPlayerDataSource(path));
    178     }
    179 
    180     private void innerSetDataSource(MediaPlayerDataSource source) {
    181         checkNotNull(source, "source");
    182         synchronized (lock) {
    183             check(!mHasBeenReleased, "has been released, reset before use");
    184             check(mDataSource == null, "cannot setDataSource more than once");
    185             mDataSource = source;
    186         }
    187     }
    188 
    189     @Override
    190     public void reset() {
    191         boolean requiresRelease;
    192         synchronized (lock) {
    193             requiresRelease = !mHasBeenReleased;
    194         }
    195         if (requiresRelease) {
    196             release();
    197         }
    198         synchronized (lock) {
    199             check(mHasBeenReleased && mIsReadyToReUse, "to re-use, must call reset after release");
    200             mDataSource = null;
    201             mIsPrepared = false;
    202             mHasDuration = false;
    203             mHasStartedPlayback = false;
    204             mEngineInitializedLatch = new CountDownLatch(1);
    205             mPlaybackFinishedLatch = new CountDownLatch(1);
    206             mHasBeenReleased = false;
    207             mIsReadyToReUse = false;
    208             mSkipCompletionReport = false;
    209             mStartPosition = 0;
    210             mDuration = 0;
    211         }
    212     }
    213 
    214     @Override
    215     public void prepare() throws IOException {
    216         MediaPlayerDataSource dataSource;
    217         int audioStreamType;
    218         synchronized (lock) {
    219             check(!mHasBeenReleased, "has been released, reset before use");
    220             check(mDataSource != null, "must setDataSource before you prepare");
    221             check(!mIsPrepared, "cannot prepare more than once");
    222             mIsPrepared = true;
    223             dataSource = mDataSource;
    224             audioStreamType = mAudioStreamType;
    225         }
    226         // NYI This should become another executable that we can wait on.
    227         MediaPlayer mediaPlayer = new MediaPlayer();
    228         mediaPlayer.setAudioStreamType(audioStreamType);
    229         dataSource.setAsSourceFor(mediaPlayer);
    230         mediaPlayer.prepare();
    231         synchronized (lock) {
    232             check(!mHasDuration, "can't have duration, this is impossible");
    233             mHasDuration = true;
    234             mDuration = mediaPlayer.getDuration();
    235         }
    236         mediaPlayer.release();
    237     }
    238 
    239     @Override
    240     public int getDuration() {
    241         synchronized (lock) {
    242             check(!mHasBeenReleased, "has been released, reset before use");
    243             check(mHasDuration, "you haven't called prepare, can't get the duration");
    244             return mDuration;
    245         }
    246     }
    247 
    248     @Override
    249     public void seekTo(int startPosition) {
    250         boolean currentlyPlaying;
    251         MediaPlayerDataSource dataSource;
    252         synchronized (lock) {
    253             check(!mHasBeenReleased, "has been released, reset before use");
    254             check(mHasDuration, "you can't seek until you have prepared");
    255             currentlyPlaying = mHasStartedPlayback && !hasPlaybackFinished();
    256             mStartPosition = Math.min(startPosition, mDuration);
    257             dataSource = mDataSource;
    258         }
    259         if (currentlyPlaying) {
    260             stopAndStartPlayingAgain(dataSource);
    261         }
    262     }
    263 
    264     private void stopAndStartPlayingAgain(MediaPlayerDataSource source) {
    265         stopCurrentPlayback();
    266         reset();
    267         innerSetDataSource(source);
    268         try {
    269             prepare();
    270         } catch (IOException e) {
    271             reportException(e);
    272             return;
    273         }
    274         start();
    275         return;
    276     }
    277 
    278     private void reportException(Exception e) {
    279         Log.e(TAG, "playback error:", e);
    280     }
    281 
    282     @Override
    283     public void start() {
    284         MediaPlayerDataSource restartWithThisDataSource = null;
    285         synchronized (lock) {
    286             check(!mHasBeenReleased, "has been released, reset before use");
    287             check(mIsPrepared, "must have prepared before you can start");
    288             if (!mHasStartedPlayback) {
    289                 // Playback has not started. Start it.
    290                 mHasStartedPlayback = true;
    291                 EngineParameters engineParameters = new EngineParameters.Builder()
    292                         .initialRate(mCurrentPlaybackRate)
    293                         .startPositionMillis(mStartPosition)
    294                         .audioStreamType(mAudioStreamType)
    295                         .build();
    296                 VariableSpeedNative.initializeEngine(engineParameters);
    297                 VariableSpeedNative.startPlayback();
    298                 mEngineInitializedLatch.countDown();
    299                 mExecutor.execute(new PlaybackRunnable(mDataSource));
    300             } else {
    301                 // Playback has already started. Restart it, without holding the
    302                 // lock.
    303                 restartWithThisDataSource = mDataSource;
    304             }
    305         }
    306         if (restartWithThisDataSource != null) {
    307             stopAndStartPlayingAgain(restartWithThisDataSource);
    308         }
    309     }
    310 
    311     /** A Runnable capable of driving the native audio playback methods. */
    312     private final class PlaybackRunnable implements Runnable {
    313         private final MediaPlayerDataSource mInnerSource;
    314 
    315         public PlaybackRunnable(MediaPlayerDataSource source) {
    316             mInnerSource = source;
    317         }
    318 
    319         @Override
    320         public void run() {
    321             try {
    322                 mInnerSource.playNative();
    323             } catch (IOException e) {
    324                 Log.e(TAG, "error playing audio", e);
    325             }
    326             MediaPlayer.OnCompletionListener completionListener;
    327             boolean skipThisCompletionReport;
    328             synchronized (lock) {
    329                 completionListener = mCompletionListener;
    330                 skipThisCompletionReport = mSkipCompletionReport;
    331                 mPlaybackFinishedLatch.countDown();
    332             }
    333             if (!skipThisCompletionReport && completionListener != null) {
    334                 completionListener.onCompletion(null);
    335             }
    336         }
    337     }
    338 
    339     @Override
    340     public boolean isPlaying() {
    341         synchronized (lock) {
    342             return mHasStartedPlayback && !hasPlaybackFinished();
    343         }
    344     }
    345 
    346     @Override
    347     public int getCurrentPosition() {
    348         synchronized (lock) {
    349             check(!mHasBeenReleased, "has been released, reset before use");
    350             if (!mHasStartedPlayback) {
    351                 return 0;
    352             }
    353             if (!hasEngineBeenInitialized()) {
    354                 return 0;
    355             }
    356             if (!hasPlaybackFinished()) {
    357                 return VariableSpeedNative.getCurrentPosition();
    358             }
    359             return mDuration;
    360         }
    361     }
    362 
    363     @Override
    364     public void pause() {
    365         synchronized (lock) {
    366             check(!mHasBeenReleased, "has been released, reset before use");
    367         }
    368         stopCurrentPlayback();
    369     }
    370 
    371     public void setVariableSpeed(float rate) {
    372         // TODO: are there situations in which the engine has been destroyed, so
    373         // that this will segfault?
    374         synchronized (lock) {
    375             check(!mHasBeenReleased, "has been released, reset before use");
    376             // TODO: This too is wrong, once we've started preparing the variable speed set
    377             // will not be enough.
    378             if (mHasStartedPlayback) {
    379                 VariableSpeedNative.setVariableSpeed(rate);
    380             }
    381             mCurrentPlaybackRate = rate;
    382         }
    383     }
    384 
    385     private void check(boolean condition, String exception) {
    386         if (!condition) {
    387             throw new IllegalStateException(exception);
    388         }
    389     }
    390 
    391     private void checkNotNull(Object argument, String argumentName) {
    392         if (argument == null) {
    393             throw new IllegalArgumentException(argumentName + " must not be null");
    394         }
    395     }
    396 
    397     @Override
    398     public void setAudioStreamType(int audioStreamType) {
    399         synchronized (lock) {
    400             mAudioStreamType = audioStreamType;
    401         }
    402     }
    403 }
    404