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