1/*
2 * Copyright (C) 2017 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 android.support.v17.leanback.media;
18
19import android.content.Context;
20import android.media.AudioManager;
21import android.media.MediaPlayer;
22import android.net.Uri;
23import android.os.Handler;
24import android.support.v17.leanback.R;
25import android.view.SurfaceHolder;
26
27import java.io.IOException;
28
29/**
30 * This implementation extends the {@link PlayerAdapter} with a {@link MediaPlayer}.
31 */
32public class MediaPlayerAdapter extends PlayerAdapter {
33
34    Context mContext;
35    final MediaPlayer mPlayer = new MediaPlayer();
36    SurfaceHolderGlueHost mSurfaceHolderGlueHost;
37    final Runnable mRunnable = new Runnable() {
38        @Override
39        public void run() {
40            getCallback().onCurrentPositionChanged(MediaPlayerAdapter.this);
41            mHandler.postDelayed(this, getUpdatePeriod());
42        }
43    };;
44    final Handler mHandler = new Handler();
45    boolean mInitialized = false; // true when the MediaPlayer is prepared/initialized
46    Uri mMediaSourceUri = null;
47    boolean mHasDisplay;
48    long mBufferedProgress;
49
50    MediaPlayer.OnPreparedListener mOnPreparedListener = new MediaPlayer.OnPreparedListener() {
51        @Override
52        public void onPrepared(MediaPlayer mp) {
53            mInitialized = true;
54            notifyBufferingStartEnd();
55            if (mSurfaceHolderGlueHost == null || mHasDisplay) {
56                getCallback().onPreparedStateChanged(MediaPlayerAdapter.this);
57            }
58        }
59    };
60
61    final MediaPlayer.OnCompletionListener mOnCompletionListener =
62            new MediaPlayer.OnCompletionListener() {
63        @Override
64        public void onCompletion(MediaPlayer mediaPlayer) {
65            getCallback().onPlayStateChanged(MediaPlayerAdapter.this);
66            getCallback().onPlayCompleted(MediaPlayerAdapter.this);
67        }
68    };
69
70    final MediaPlayer.OnBufferingUpdateListener mOnBufferingUpdateListener =
71            new MediaPlayer.OnBufferingUpdateListener() {
72        @Override
73        public void onBufferingUpdate(MediaPlayer mp, int percent) {
74            mBufferedProgress = getDuration() * percent / 100;
75            getCallback().onBufferedPositionChanged(MediaPlayerAdapter.this);
76        }
77    };
78
79    final MediaPlayer.OnVideoSizeChangedListener mOnVideoSizeChangedListener =
80            new MediaPlayer.OnVideoSizeChangedListener() {
81        @Override
82        public void onVideoSizeChanged(MediaPlayer mediaPlayer, int width, int height) {
83            getCallback().onVideoSizeChanged(MediaPlayerAdapter.this, width, height);
84        }
85    };
86
87    final MediaPlayer.OnErrorListener mOnErrorListener =
88            new MediaPlayer.OnErrorListener() {
89                @Override
90                public boolean onError(MediaPlayer mp, int what, int extra) {
91                    getCallback().onError(MediaPlayerAdapter.this, what,
92                            mContext.getString(R.string.lb_media_player_error, what, extra));
93                    return MediaPlayerAdapter.this.onError(what, extra);
94                }
95            };
96
97    final MediaPlayer.OnSeekCompleteListener mOnSeekCompleteListener =
98            new MediaPlayer.OnSeekCompleteListener() {
99                @Override
100                public void onSeekComplete(MediaPlayer mp) {
101                    MediaPlayerAdapter.this.onSeekComplete();
102                }
103            };
104
105    final MediaPlayer.OnInfoListener mOnInfoListener = new MediaPlayer.OnInfoListener() {
106        @Override
107        public boolean onInfo(MediaPlayer mp, int what, int extra) {
108            boolean handled = false;
109            switch (what) {
110                case MediaPlayer.MEDIA_INFO_BUFFERING_START:
111                    mBufferingStart = true;
112                    notifyBufferingStartEnd();
113                    handled = true;
114                    break;
115                case MediaPlayer.MEDIA_INFO_BUFFERING_END:
116                    mBufferingStart = false;
117                    notifyBufferingStartEnd();
118                    handled = true;
119                    break;
120            }
121            boolean thisHandled = MediaPlayerAdapter.this.onInfo(what, extra);
122            return handled || thisHandled;
123        }
124    };
125
126    boolean mBufferingStart;
127
128    void notifyBufferingStartEnd() {
129        getCallback().onBufferingStateChanged(MediaPlayerAdapter.this,
130                mBufferingStart || !mInitialized);
131    }
132
133    /**
134     * Constructor.
135     */
136    public MediaPlayerAdapter(Context context) {
137        mContext = context;
138    }
139
140    @Override
141    public void onAttachedToHost(PlaybackGlueHost host) {
142        if (host instanceof SurfaceHolderGlueHost) {
143            mSurfaceHolderGlueHost = ((SurfaceHolderGlueHost) host);
144            mSurfaceHolderGlueHost.setSurfaceHolderCallback(new VideoPlayerSurfaceHolderCallback());
145        }
146    }
147
148    /**
149     * Will reset the {@link MediaPlayer} and the glue such that a new file can be played. You are
150     * not required to call this method before playing the first file. However you have to call it
151     * before playing a second one.
152     */
153    public void reset() {
154        changeToUnitialized();
155        mPlayer.reset();
156    }
157
158    void changeToUnitialized() {
159        if (mInitialized) {
160            mInitialized = false;
161            notifyBufferingStartEnd();
162            if (mHasDisplay) {
163                getCallback().onPreparedStateChanged(MediaPlayerAdapter.this);
164            }
165        }
166    }
167
168    /**
169     * Release internal MediaPlayer. Should not use the object after call release().
170     */
171    public void release() {
172        changeToUnitialized();
173        mHasDisplay = false;
174        mPlayer.release();
175    }
176
177    @Override
178    public void onDetachedFromHost() {
179        if (mSurfaceHolderGlueHost != null) {
180            mSurfaceHolderGlueHost.setSurfaceHolderCallback(null);
181            mSurfaceHolderGlueHost = null;
182        }
183        reset();
184        release();
185    }
186
187    /**
188     * Called to indicate an error.
189     *
190     * @param what    the type of error that has occurred:
191     * <ul>
192     * <li>{@link MediaPlayer#MEDIA_ERROR_UNKNOWN}
193     * <li>{@link MediaPlayer#MEDIA_ERROR_SERVER_DIED}
194     * </ul>
195     * @param extra an extra code, specific to the error. Typically
196     * implementation dependent.
197     * <ul>
198     * <li>{@link MediaPlayer#MEDIA_ERROR_IO}
199     * <li>{@link MediaPlayer#MEDIA_ERROR_MALFORMED}
200     * <li>{@link MediaPlayer#MEDIA_ERROR_UNSUPPORTED}
201     * <li>{@link MediaPlayer#MEDIA_ERROR_TIMED_OUT}
202     * <li><code>MEDIA_ERROR_SYSTEM (-2147483648)</code> - low-level system error.
203     * </ul>
204     * @return True if the method handled the error, false if it didn't.
205     * Returning false, will cause the {@link PlayerAdapter.Callback#onPlayCompleted(PlayerAdapter)}
206     * being called.
207     */
208    protected boolean onError(int what, int extra) {
209        return false;
210    }
211
212    /**
213     * Called to indicate the completion of a seek operation.
214     */
215    protected void onSeekComplete() {
216    }
217
218    /**
219     * Called to indicate an info or a warning.
220     *
221     * @param what    the type of info or warning.
222     * <ul>
223     * <li>{@link MediaPlayer#MEDIA_INFO_UNKNOWN}
224     * <li>{@link MediaPlayer#MEDIA_INFO_VIDEO_TRACK_LAGGING}
225     * <li>{@link MediaPlayer#MEDIA_INFO_VIDEO_RENDERING_START}
226     * <li>{@link MediaPlayer#MEDIA_INFO_BUFFERING_START}
227     * <li>{@link MediaPlayer#MEDIA_INFO_BUFFERING_END}
228     * <li><code>MEDIA_INFO_NETWORK_BANDWIDTH (703)</code> -
229     *     bandwidth information is available (as <code>extra</code> kbps)
230     * <li>{@link MediaPlayer#MEDIA_INFO_BAD_INTERLEAVING}
231     * <li>{@link MediaPlayer#MEDIA_INFO_NOT_SEEKABLE}
232     * <li>{@link MediaPlayer#MEDIA_INFO_METADATA_UPDATE}
233     * <li>{@link MediaPlayer#MEDIA_INFO_UNSUPPORTED_SUBTITLE}
234     * <li>{@link MediaPlayer#MEDIA_INFO_SUBTITLE_TIMED_OUT}
235     * </ul>
236     * @param extra an extra code, specific to the info. Typically
237     * implementation dependent.
238     * @return True if the method handled the info, false if it didn't.
239     * Returning false, will cause the info to be discarded.
240     */
241    protected boolean onInfo(int what, int extra) {
242        return false;
243    }
244
245    /**
246     * @see MediaPlayer#setDisplay(SurfaceHolder)
247     */
248    void setDisplay(SurfaceHolder surfaceHolder) {
249        boolean hadDisplay = mHasDisplay;
250        mHasDisplay = surfaceHolder != null;
251        if (hadDisplay == mHasDisplay) {
252            return;
253        }
254        mPlayer.setDisplay(surfaceHolder);
255        if (mHasDisplay) {
256            if (mInitialized) {
257                getCallback().onPreparedStateChanged(MediaPlayerAdapter.this);
258            }
259        } else {
260            if (mInitialized) {
261                getCallback().onPreparedStateChanged(MediaPlayerAdapter.this);
262            }
263        }
264
265    }
266
267    @Override
268    public void setProgressUpdatingEnabled(final boolean enabled) {
269        mHandler.removeCallbacks(mRunnable);
270        if (!enabled) {
271            return;
272        }
273        mHandler.postDelayed(mRunnable, getUpdatePeriod());
274    }
275
276    int getUpdatePeriod() {
277        return 16;
278    }
279
280    @Override
281    public boolean isPlaying() {
282        return mInitialized && mPlayer.isPlaying();
283    }
284
285    @Override
286    public long getDuration() {
287        return mInitialized ? mPlayer.getDuration() : -1;
288    }
289
290    @Override
291    public long getCurrentPosition() {
292        return mInitialized ? mPlayer.getCurrentPosition() : -1;
293    }
294
295    @Override
296    public void play() {
297        if (!mInitialized || mPlayer.isPlaying()) {
298            return;
299        }
300        mPlayer.start();
301        getCallback().onPlayStateChanged(MediaPlayerAdapter.this);
302        getCallback().onCurrentPositionChanged(MediaPlayerAdapter.this);
303    }
304
305    @Override
306    public void pause() {
307        if (isPlaying()) {
308            mPlayer.pause();
309            getCallback().onPlayStateChanged(MediaPlayerAdapter.this);
310        }
311    }
312
313    @Override
314    public void seekTo(long newPosition) {
315        if (!mInitialized) {
316            return;
317        }
318        mPlayer.seekTo((int) newPosition);
319    }
320
321    @Override
322    public long getBufferedPosition() {
323        return mBufferedProgress;
324    }
325
326    /**
327     * Sets the media source of the player witha given URI.
328     *
329     * @return Returns <code>true</code> if uri represents a new media; <code>false</code>
330     * otherwise.
331     * @see MediaPlayer#setDataSource(String)
332     */
333    public boolean setDataSource(Uri uri) {
334        if (mMediaSourceUri != null ? mMediaSourceUri.equals(uri) : uri == null) {
335            return false;
336        }
337        mMediaSourceUri = uri;
338        prepareMediaForPlaying();
339        return true;
340    }
341
342    private void prepareMediaForPlaying() {
343        reset();
344        try {
345            if (mMediaSourceUri != null) {
346                mPlayer.setDataSource(mContext, mMediaSourceUri);
347            } else {
348                return;
349            }
350        } catch (IOException e) {
351            e.printStackTrace();
352            throw new RuntimeException(e);
353        }
354        mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
355        mPlayer.setOnPreparedListener(mOnPreparedListener);
356        mPlayer.setOnVideoSizeChangedListener(mOnVideoSizeChangedListener);
357        mPlayer.setOnErrorListener(mOnErrorListener);
358        mPlayer.setOnSeekCompleteListener(mOnSeekCompleteListener);
359        mPlayer.setOnCompletionListener(mOnCompletionListener);
360        mPlayer.setOnInfoListener(mOnInfoListener);
361        mPlayer.setOnBufferingUpdateListener(mOnBufferingUpdateListener);
362        notifyBufferingStartEnd();
363        mPlayer.prepareAsync();
364        getCallback().onPlayStateChanged(MediaPlayerAdapter.this);
365    }
366
367    /**
368     * @return True if MediaPlayer OnPreparedListener is invoked and got a SurfaceHolder if
369     * {@link PlaybackGlueHost} provides SurfaceHolder.
370     */
371    @Override
372    public boolean isPrepared() {
373        return mInitialized && (mSurfaceHolderGlueHost == null || mHasDisplay);
374    }
375
376    /**
377     * Implements {@link SurfaceHolder.Callback} that can then be set on the
378     * {@link PlaybackGlueHost}.
379     */
380    class VideoPlayerSurfaceHolderCallback implements SurfaceHolder.Callback {
381        @Override
382        public void surfaceCreated(SurfaceHolder surfaceHolder) {
383            setDisplay(surfaceHolder);
384        }
385
386        @Override
387        public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
388        }
389
390        @Override
391        public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
392            setDisplay(null);
393        }
394    }
395}
396