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