/* * Copyright (C) 2011 The Android Open Source Project * * 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. */ package com.android.ex.variablespeed; import com.google.common.base.Preconditions; import android.content.Context; import android.media.MediaPlayer; import android.net.Uri; import android.util.Log; import java.io.IOException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; /** * This class behaves in a similar fashion to the MediaPlayer, but by using * native code it is able to use variable-speed playback. *

* This class is thread-safe. It's not yet perfect though, see the unit tests * for details - there is insufficient testing for the concurrent logic. You are * probably best advised to use thread confinment until the unit tests are more * complete with regards to threading. *

* The easiest way to ensure that calls to this class are not made concurrently * (besides only ever accessing it from one thread) is to wrap it in a * {@link SingleThreadedMediaPlayerProxy}, designed just for this purpose. */ @ThreadSafe public class VariableSpeed implements MediaPlayerProxy { private static final String TAG = "VariableSpeed"; private final Executor mExecutor; private final Object lock = new Object(); @GuardedBy("lock") private MediaPlayerDataSource mDataSource; @GuardedBy("lock") private boolean mIsPrepared; @GuardedBy("lock") private boolean mHasDuration; @GuardedBy("lock") private boolean mHasStartedPlayback; @GuardedBy("lock") private CountDownLatch mEngineInitializedLatch; @GuardedBy("lock") private CountDownLatch mPlaybackFinishedLatch; @GuardedBy("lock") private boolean mHasBeenReleased = true; @GuardedBy("lock") private boolean mIsReadyToReUse = true; @GuardedBy("lock") private boolean mSkipCompletionReport; @GuardedBy("lock") private int mStartPosition; @GuardedBy("lock") private float mCurrentPlaybackRate = 1.0f; @GuardedBy("lock") private int mDuration; @GuardedBy("lock") private MediaPlayer.OnCompletionListener mCompletionListener; @GuardedBy("lock") private int mAudioStreamType; private VariableSpeed(Executor executor) throws UnsupportedOperationException { Preconditions.checkNotNull(executor); mExecutor = executor; try { VariableSpeedNative.loadLibrary(); } catch (UnsatisfiedLinkError e) { throw new UnsupportedOperationException("could not load library", e); } catch (SecurityException e) { throw new UnsupportedOperationException("could not load library", e); } reset(); } public static MediaPlayerProxy createVariableSpeed(Executor executor) throws UnsupportedOperationException { return new SingleThreadedMediaPlayerProxy(new VariableSpeed(executor)); } @Override public void setOnCompletionListener(MediaPlayer.OnCompletionListener listener) { synchronized (lock) { check(!mHasBeenReleased, "has been released, reset before use"); mCompletionListener = listener; } } @Override public void setOnErrorListener(MediaPlayer.OnErrorListener listener) { synchronized (lock) { check(!mHasBeenReleased, "has been released, reset before use"); // TODO: I haven't actually added any error listener code. } } @Override public void release() { synchronized (lock) { if (mHasBeenReleased) { return; } mHasBeenReleased = true; } stopCurrentPlayback(); boolean requiresShutdown = false; synchronized (lock) { requiresShutdown = hasEngineBeenInitialized(); } if (requiresShutdown) { VariableSpeedNative.shutdownEngine(); } synchronized (lock) { mIsReadyToReUse = true; } } private boolean hasEngineBeenInitialized() { return mEngineInitializedLatch.getCount() <= 0; } private boolean hasPlaybackFinished() { return mPlaybackFinishedLatch.getCount() <= 0; } /** * Stops the current playback, returns once it has stopped. */ private void stopCurrentPlayback() { boolean isPlaying; CountDownLatch engineInitializedLatch; CountDownLatch playbackFinishedLatch; synchronized (lock) { isPlaying = mHasStartedPlayback && !hasPlaybackFinished(); engineInitializedLatch = mEngineInitializedLatch; playbackFinishedLatch = mPlaybackFinishedLatch; if (isPlaying) { mSkipCompletionReport = true; } } if (isPlaying) { waitForLatch(engineInitializedLatch); VariableSpeedNative.stopPlayback(); waitForLatch(playbackFinishedLatch); } } private void waitForLatch(CountDownLatch latch) { try { boolean success = latch.await(1, TimeUnit.SECONDS); if (!success) { reportException(new TimeoutException("waited too long")); } } catch (InterruptedException e) { // Preserve the interrupt status, though this is unexpected. Thread.currentThread().interrupt(); reportException(e); } } @Override public void setDataSource(Context context, Uri intentUri) { checkNotNull(context, "context"); checkNotNull(intentUri, "intentUri"); innerSetDataSource(new MediaPlayerDataSource(context, intentUri)); } @Override public void setDataSource(String path) { checkNotNull(path, "path"); innerSetDataSource(new MediaPlayerDataSource(path)); } private void innerSetDataSource(MediaPlayerDataSource source) { checkNotNull(source, "source"); synchronized (lock) { check(!mHasBeenReleased, "has been released, reset before use"); check(mDataSource == null, "cannot setDataSource more than once"); mDataSource = source; } } @Override public void reset() { boolean requiresRelease; synchronized (lock) { requiresRelease = !mHasBeenReleased; } if (requiresRelease) { release(); } synchronized (lock) { check(mHasBeenReleased && mIsReadyToReUse, "to re-use, must call reset after release"); mDataSource = null; mIsPrepared = false; mHasDuration = false; mHasStartedPlayback = false; mEngineInitializedLatch = new CountDownLatch(1); mPlaybackFinishedLatch = new CountDownLatch(1); mHasBeenReleased = false; mIsReadyToReUse = false; mSkipCompletionReport = false; mStartPosition = 0; mDuration = 0; } } @Override public void prepare() throws IOException { MediaPlayerDataSource dataSource; int audioStreamType; synchronized (lock) { check(!mHasBeenReleased, "has been released, reset before use"); check(mDataSource != null, "must setDataSource before you prepare"); check(!mIsPrepared, "cannot prepare more than once"); mIsPrepared = true; dataSource = mDataSource; audioStreamType = mAudioStreamType; } // NYI This should become another executable that we can wait on. MediaPlayer mediaPlayer = new MediaPlayer(); mediaPlayer.setAudioStreamType(audioStreamType); dataSource.setAsSourceFor(mediaPlayer); mediaPlayer.prepare(); synchronized (lock) { check(!mHasDuration, "can't have duration, this is impossible"); mHasDuration = true; mDuration = mediaPlayer.getDuration(); } mediaPlayer.release(); } @Override public int getDuration() { synchronized (lock) { check(!mHasBeenReleased, "has been released, reset before use"); check(mHasDuration, "you haven't called prepare, can't get the duration"); return mDuration; } } @Override public void seekTo(int startPosition) { boolean currentlyPlaying; MediaPlayerDataSource dataSource; synchronized (lock) { check(!mHasBeenReleased, "has been released, reset before use"); check(mHasDuration, "you can't seek until you have prepared"); currentlyPlaying = mHasStartedPlayback && !hasPlaybackFinished(); mStartPosition = Math.min(startPosition, mDuration); dataSource = mDataSource; } if (currentlyPlaying) { stopAndStartPlayingAgain(dataSource); } } private void stopAndStartPlayingAgain(MediaPlayerDataSource source) { stopCurrentPlayback(); reset(); innerSetDataSource(source); try { prepare(); } catch (IOException e) { reportException(e); return; } start(); return; } private void reportException(Exception e) { Log.e(TAG, "playback error:", e); } @Override public void start() { MediaPlayerDataSource restartWithThisDataSource = null; synchronized (lock) { check(!mHasBeenReleased, "has been released, reset before use"); check(mIsPrepared, "must have prepared before you can start"); if (!mHasStartedPlayback) { // Playback has not started. Start it. mHasStartedPlayback = true; EngineParameters engineParameters = new EngineParameters.Builder() .initialRate(mCurrentPlaybackRate) .startPositionMillis(mStartPosition) .audioStreamType(mAudioStreamType) .build(); VariableSpeedNative.initializeEngine(engineParameters); VariableSpeedNative.startPlayback(); mEngineInitializedLatch.countDown(); mExecutor.execute(new PlaybackRunnable(mDataSource)); } else { // Playback has already started. Restart it, without holding the // lock. restartWithThisDataSource = mDataSource; } } if (restartWithThisDataSource != null) { stopAndStartPlayingAgain(restartWithThisDataSource); } } /** A Runnable capable of driving the native audio playback methods. */ private final class PlaybackRunnable implements Runnable { private final MediaPlayerDataSource mInnerSource; public PlaybackRunnable(MediaPlayerDataSource source) { mInnerSource = source; } @Override public void run() { try { mInnerSource.playNative(); } catch (IOException e) { Log.e(TAG, "error playing audio", e); } MediaPlayer.OnCompletionListener completionListener; boolean skipThisCompletionReport; synchronized (lock) { completionListener = mCompletionListener; skipThisCompletionReport = mSkipCompletionReport; mPlaybackFinishedLatch.countDown(); } if (!skipThisCompletionReport && completionListener != null) { completionListener.onCompletion(null); } } } @Override public boolean isPlaying() { synchronized (lock) { return mHasStartedPlayback && !hasPlaybackFinished(); } } @Override public int getCurrentPosition() { synchronized (lock) { check(!mHasBeenReleased, "has been released, reset before use"); if (!mHasStartedPlayback) { return 0; } if (!hasEngineBeenInitialized()) { return 0; } if (!hasPlaybackFinished()) { return VariableSpeedNative.getCurrentPosition(); } return mDuration; } } @Override public void pause() { synchronized (lock) { check(!mHasBeenReleased, "has been released, reset before use"); } stopCurrentPlayback(); } public void setVariableSpeed(float rate) { // TODO: are there situations in which the engine has been destroyed, so // that this will segfault? synchronized (lock) { check(!mHasBeenReleased, "has been released, reset before use"); // TODO: This too is wrong, once we've started preparing the variable speed set // will not be enough. if (mHasStartedPlayback) { VariableSpeedNative.setVariableSpeed(rate); } mCurrentPlaybackRate = rate; } } private void check(boolean condition, String exception) { if (!condition) { throw new IllegalStateException(exception); } } private void checkNotNull(Object argument, String argumentName) { if (argument == null) { throw new IllegalArgumentException(argumentName + " must not be null"); } } @Override public void setAudioStreamType(int audioStreamType) { synchronized (lock) { mAudioStreamType = audioStreamType; } } }