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