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 com.google.common.base.Preconditions;
20
21import android.content.Context;
22import android.media.MediaPlayer;
23import android.net.Uri;
24import android.util.Log;
25
26import java.io.IOException;
27import java.util.concurrent.CountDownLatch;
28import java.util.concurrent.Executor;
29import java.util.concurrent.TimeUnit;
30import java.util.concurrent.TimeoutException;
31
32import javax.annotation.concurrent.GuardedBy;
33import 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
49public 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 isReadyToPlay() {
341        synchronized (lock) {
342            return !mHasBeenReleased && mHasDuration;
343        }
344    }
345
346    @Override
347    public boolean isPlaying() {
348        synchronized (lock) {
349            return isReadyToPlay() && mHasStartedPlayback && !hasPlaybackFinished();
350        }
351    }
352
353    @Override
354    public int getCurrentPosition() {
355        synchronized (lock) {
356            check(!mHasBeenReleased, "has been released, reset before use");
357            if (!mHasStartedPlayback) {
358                return 0;
359            }
360            if (!hasEngineBeenInitialized()) {
361                return 0;
362            }
363            if (!hasPlaybackFinished()) {
364                return VariableSpeedNative.getCurrentPosition();
365            }
366            return mDuration;
367        }
368    }
369
370    @Override
371    public void pause() {
372        synchronized (lock) {
373            check(!mHasBeenReleased, "has been released, reset before use");
374        }
375        stopCurrentPlayback();
376    }
377
378    public void setVariableSpeed(float rate) {
379        // TODO: are there situations in which the engine has been destroyed, so
380        // that this will segfault?
381        synchronized (lock) {
382            check(!mHasBeenReleased, "has been released, reset before use");
383            // TODO: This too is wrong, once we've started preparing the variable speed set
384            // will not be enough.
385            if (mHasStartedPlayback) {
386                VariableSpeedNative.setVariableSpeed(rate);
387            }
388            mCurrentPlaybackRate = rate;
389        }
390    }
391
392    private void check(boolean condition, String exception) {
393        if (!condition) {
394            throw new IllegalStateException(exception);
395        }
396    }
397
398    private void checkNotNull(Object argument, String argumentName) {
399        if (argument == null) {
400            throw new IllegalArgumentException(argumentName + " must not be null");
401        }
402    }
403
404    @Override
405    public void setAudioStreamType(int audioStreamType) {
406        synchronized (lock) {
407            mAudioStreamType = audioStreamType;
408        }
409    }
410}
411