MediaPlayerGlue.java revision b31c3281d870e9abb673db239234d580dcc4feff
1/*
2 * Copyright (C) 2016 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 androidx.leanback.media;
18
19import android.content.Context;
20import android.graphics.drawable.Drawable;
21import android.media.AudioManager;
22import android.media.MediaPlayer;
23import android.net.Uri;
24import android.os.Handler;
25import androidx.annotation.RestrictTo;
26import androidx.leanback.widget.Action;
27import androidx.leanback.widget.ArrayObjectAdapter;
28import androidx.leanback.widget.OnItemViewSelectedListener;
29import androidx.leanback.widget.PlaybackControlsRow;
30import androidx.leanback.widget.Presenter;
31import androidx.leanback.widget.Row;
32import androidx.leanback.widget.RowPresenter;
33import android.view.KeyEvent;
34import android.view.SurfaceHolder;
35import android.view.View;
36
37import java.io.IOException;
38import java.util.List;
39
40/**
41 * This glue extends the {@link androidx.leanback.media.PlaybackControlGlue} with a
42 * {@link MediaPlayer} synchronization. It supports 7 actions:
43 *
44 * <ul>
45 * <li>{@link androidx.leanback.widget.PlaybackControlsRow.FastForwardAction}</li>
46 * <li>{@link androidx.leanback.widget.PlaybackControlsRow.RewindAction}</li>
47 * <li>{@link  androidx.leanback.widget.PlaybackControlsRow.PlayPauseAction}</li>
48 * <li>{@link androidx.leanback.widget.PlaybackControlsRow.RepeatAction}</li>
49 * <li>{@link androidx.leanback.widget.PlaybackControlsRow.ThumbsDownAction}</li>
50 * <li>{@link androidx.leanback.widget.PlaybackControlsRow.ThumbsUpAction}</li>
51 * </ul>
52 *
53 * @hide
54 * @deprecated Use {@link MediaPlayerAdapter} with {@link PlaybackTransportControlGlue} or
55 *             {@link PlaybackBannerControlGlue}.
56 */
57@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
58@Deprecated
59public class MediaPlayerGlue extends PlaybackControlGlue implements
60        OnItemViewSelectedListener {
61
62    public static final int NO_REPEAT = 0;
63    public static final int REPEAT_ONE = 1;
64    public static final int REPEAT_ALL = 2;
65
66    public static final int FAST_FORWARD_REWIND_STEP = 10 * 1000; // in milliseconds
67    public static final int FAST_FORWARD_REWIND_REPEAT_DELAY = 200; // in milliseconds
68    private static final String TAG = "MediaPlayerGlue";
69    protected final PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
70    protected final PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
71    MediaPlayer mPlayer = new MediaPlayer();
72    private final PlaybackControlsRow.RepeatAction mRepeatAction;
73    private Runnable mRunnable;
74    private Handler mHandler = new Handler();
75    private boolean mInitialized = false; // true when the MediaPlayer is prepared/initialized
76    private Action mSelectedAction; // the action which is currently selected by the user
77    private long mLastKeyDownEvent = 0L; // timestamp when the last DPAD_CENTER KEY_DOWN occurred
78    private Uri mMediaSourceUri = null;
79    private String mMediaSourcePath = null;
80    private MediaPlayer.OnCompletionListener mOnCompletionListener;
81    private String mArtist;
82    private String mTitle;
83    private Drawable mCover;
84
85    /**
86     * Sets the drawable representing cover image.
87     */
88    public void setCover(Drawable cover) {
89        this.mCover = cover;
90    }
91
92    /**
93     * Sets the artist name.
94     */
95    public void setArtist(String artist) {
96        this.mArtist = artist;
97    }
98
99    /**
100     * Sets the media title.
101     */
102    public void setTitle(String title) {
103        this.mTitle = title;
104    }
105
106    /**
107     * Sets the url for the video.
108     */
109    public void setVideoUrl(String videoUrl) {
110        setMediaSource(videoUrl);
111        onMetadataChanged();
112    }
113
114    /**
115     * Constructor.
116     */
117    public MediaPlayerGlue(Context context) {
118        this(context, new int[]{1}, new int[]{1});
119    }
120
121    /**
122     * Constructor.
123     */
124    public MediaPlayerGlue(
125            Context context, int[] fastForwardSpeeds, int[] rewindSpeeds) {
126        super(context, fastForwardSpeeds, rewindSpeeds);
127
128        // Instantiate secondary actions
129        mRepeatAction = new PlaybackControlsRow.RepeatAction(getContext());
130        mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(getContext());
131        mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(getContext());
132        mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
133        mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
134    }
135
136    @Override
137    protected void onAttachedToHost(PlaybackGlueHost host) {
138        super.onAttachedToHost(host);
139        if (host instanceof SurfaceHolderGlueHost) {
140            ((SurfaceHolderGlueHost) host).setSurfaceHolderCallback(
141                    new VideoPlayerSurfaceHolderCallback());
142        }
143    }
144
145    /**
146     * Will reset the {@link MediaPlayer} and the glue such that a new file can be played. You are
147     * not required to call this method before playing the first file. However you have to call it
148     * before playing a second one.
149     */
150    public void reset() {
151        changeToUnitialized();
152        mPlayer.reset();
153    }
154
155    void changeToUnitialized() {
156        if (mInitialized) {
157            mInitialized = false;
158            List<PlayerCallback> callbacks = getPlayerCallbacks();
159            if (callbacks != null) {
160                for (PlayerCallback callback: callbacks) {
161                    callback.onPreparedStateChanged(MediaPlayerGlue.this);
162                }
163            }
164        }
165    }
166
167    /**
168     * Release internal MediaPlayer. Should not use the object after call release().
169     */
170    public void release() {
171        changeToUnitialized();
172        mPlayer.release();
173    }
174
175    @Override
176    protected void onDetachedFromHost() {
177        if (getHost() instanceof SurfaceHolderGlueHost) {
178            ((SurfaceHolderGlueHost) getHost()).setSurfaceHolderCallback(null);
179        }
180        reset();
181        release();
182        super.onDetachedFromHost();
183    }
184
185    @Override
186    protected void onCreateSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter) {
187        secondaryActionsAdapter.add(mRepeatAction);
188        secondaryActionsAdapter.add(mThumbsDownAction);
189        secondaryActionsAdapter.add(mThumbsUpAction);
190    }
191
192    /**
193     * @see MediaPlayer#setDisplay(SurfaceHolder)
194     */
195    public void setDisplay(SurfaceHolder surfaceHolder) {
196        mPlayer.setDisplay(surfaceHolder);
197    }
198
199    @Override
200    public void enableProgressUpdating(final boolean enabled) {
201        if (mRunnable != null) mHandler.removeCallbacks(mRunnable);
202        if (!enabled) {
203            return;
204        }
205        if (mRunnable == null) {
206            mRunnable = new Runnable() {
207                @Override
208                public void run() {
209                    updateProgress();
210                    mHandler.postDelayed(this, getUpdatePeriod());
211                }
212            };
213        }
214        mHandler.postDelayed(mRunnable, getUpdatePeriod());
215    }
216
217    @Override
218    public void onActionClicked(Action action) {
219        // If either 'Shuffle' or 'Repeat' has been clicked we need to make sure the actions index
220        // is incremented and the UI updated such that we can display the new state.
221        super.onActionClicked(action);
222        if (action instanceof PlaybackControlsRow.RepeatAction) {
223            ((PlaybackControlsRow.RepeatAction) action).nextIndex();
224        } else if (action == mThumbsUpAction) {
225            if (mThumbsUpAction.getIndex() == PlaybackControlsRow.ThumbsAction.INDEX_SOLID) {
226                mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
227            } else {
228                mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_SOLID);
229                mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
230            }
231        } else if (action == mThumbsDownAction) {
232            if (mThumbsDownAction.getIndex() == PlaybackControlsRow.ThumbsAction.INDEX_SOLID) {
233                mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
234            } else {
235                mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_SOLID);
236                mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
237            }
238        }
239        onMetadataChanged();
240    }
241
242    @Override
243    public boolean onKey(View v, int keyCode, KeyEvent event) {
244        // This method is overridden in order to make implement fast forwarding and rewinding when
245        // the user keeps the corresponding action pressed.
246        // We only consume DPAD_CENTER Action_DOWN events on the Fast-Forward and Rewind action and
247        // only if it has not been pressed in the last X milliseconds.
248        boolean consume = mSelectedAction instanceof PlaybackControlsRow.RewindAction;
249        consume = consume || mSelectedAction instanceof PlaybackControlsRow.FastForwardAction;
250        consume = consume && mInitialized;
251        consume = consume && event.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER;
252        consume = consume && event.getAction() == KeyEvent.ACTION_DOWN;
253        consume = consume && System
254                .currentTimeMillis() - mLastKeyDownEvent > FAST_FORWARD_REWIND_REPEAT_DELAY;
255
256        if (consume) {
257            mLastKeyDownEvent = System.currentTimeMillis();
258            int newPosition = getCurrentPosition() + FAST_FORWARD_REWIND_STEP;
259            if (mSelectedAction instanceof PlaybackControlsRow.RewindAction) {
260                newPosition = getCurrentPosition() - FAST_FORWARD_REWIND_STEP;
261            }
262            // Make sure the new calculated duration is in the range 0 >= X >= MediaDuration
263            if (newPosition < 0) newPosition = 0;
264            if (newPosition > getMediaDuration()) newPosition = getMediaDuration();
265            seekTo(newPosition);
266            return true;
267        }
268
269        return super.onKey(v, keyCode, event);
270    }
271
272    @Override
273    public boolean hasValidMedia() {
274        return mTitle != null && (mMediaSourcePath != null || mMediaSourceUri != null);
275    }
276
277    @Override
278    public boolean isMediaPlaying() {
279        return mInitialized && mPlayer.isPlaying();
280    }
281
282    @Override
283    public boolean isPlaying() {
284        return isMediaPlaying();
285    }
286
287    @Override
288    public CharSequence getMediaTitle() {
289        return mTitle != null ? mTitle : "N/a";
290    }
291
292    @Override
293    public CharSequence getMediaSubtitle() {
294        return mArtist != null ? mArtist : "N/a";
295    }
296
297    @Override
298    public int getMediaDuration() {
299        return mInitialized ? mPlayer.getDuration() : 0;
300    }
301
302    @Override
303    public Drawable getMediaArt() {
304        return mCover;
305    }
306
307    @Override
308    public long getSupportedActions() {
309        return PlaybackControlGlue.ACTION_PLAY_PAUSE
310                | PlaybackControlGlue.ACTION_FAST_FORWARD
311                | PlaybackControlGlue.ACTION_REWIND;
312    }
313
314    @Override
315    public int getCurrentSpeedId() {
316        // 0 = Pause, 1 = Normal Playback Speed
317        return isMediaPlaying() ? 1 : 0;
318    }
319
320    @Override
321    public int getCurrentPosition() {
322        return mInitialized ? mPlayer.getCurrentPosition() : 0;
323    }
324
325    @Override
326    public void play(int speed) {
327        if (!mInitialized || mPlayer.isPlaying()) {
328            return;
329        }
330        mPlayer.start();
331        onMetadataChanged();
332        onStateChanged();
333        updateProgress();
334    }
335
336    @Override
337    public void pause() {
338        if (isMediaPlaying()) {
339            mPlayer.pause();
340            onStateChanged();
341        }
342    }
343
344    /**
345     * Sets the playback mode. It currently support no repeat, repeat once and infinite
346     * loop mode.
347     */
348    public void setMode(int mode) {
349        switch(mode) {
350            case NO_REPEAT:
351                mOnCompletionListener = null;
352                break;
353            case REPEAT_ONE:
354                mOnCompletionListener = new MediaPlayer.OnCompletionListener() {
355                    public boolean mFirstRepeat;
356
357                    @Override
358                    public void onCompletion(MediaPlayer mediaPlayer) {
359                        if (!mFirstRepeat) {
360                            mFirstRepeat = true;
361                            mediaPlayer.setOnCompletionListener(null);
362                        }
363                        play();
364                    }
365                };
366                break;
367            case REPEAT_ALL:
368                mOnCompletionListener = new MediaPlayer.OnCompletionListener() {
369                    @Override
370                    public void onCompletion(MediaPlayer mediaPlayer) {
371                        play();
372                    }
373                };
374                break;
375        }
376    }
377
378    /**
379     * Called whenever the user presses fast-forward/rewind or when the user keeps the
380     * corresponding action pressed.
381     *
382     * @param newPosition The new position of the media track in milliseconds.
383     */
384    protected void seekTo(int newPosition) {
385        if (!mInitialized) {
386            return;
387        }
388        mPlayer.seekTo(newPosition);
389    }
390
391    /**
392     * Sets the media source of the player witha given URI.
393     *
394     * @return Returns <code>true</code> if uri represents a new media; <code>false</code>
395     * otherwise.
396     * @see MediaPlayer#setDataSource(String)
397     */
398    public boolean setMediaSource(Uri uri) {
399        if (mMediaSourceUri != null ? mMediaSourceUri.equals(uri) : uri == null) {
400            return false;
401        }
402        mMediaSourceUri = uri;
403        mMediaSourcePath = null;
404        prepareMediaForPlaying();
405        return true;
406    }
407
408    /**
409     * Sets the media source of the player with a String path URL.
410     *
411     * @return Returns <code>true</code> if path represents a new media; <code>false</code>
412     * otherwise.
413     * @see MediaPlayer#setDataSource(String)
414     */
415    public boolean setMediaSource(String path) {
416        if (mMediaSourcePath != null ? mMediaSourcePath.equals(path) : path == null) {
417            return false;
418        }
419        mMediaSourceUri = null;
420        mMediaSourcePath = path;
421        prepareMediaForPlaying();
422        return true;
423    }
424
425    private void prepareMediaForPlaying() {
426        reset();
427        try {
428            if (mMediaSourceUri != null) {
429                mPlayer.setDataSource(getContext(), mMediaSourceUri);
430            } else if (mMediaSourcePath != null) {
431                mPlayer.setDataSource(mMediaSourcePath);
432            } else {
433                return;
434            }
435        } catch (IOException e) {
436            e.printStackTrace();
437            throw new RuntimeException(e);
438        }
439        mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
440        mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
441            @Override
442            public void onPrepared(MediaPlayer mp) {
443                mInitialized = true;
444                List<PlayerCallback> callbacks = getPlayerCallbacks();
445                if (callbacks != null) {
446                    for (PlayerCallback callback: callbacks) {
447                        callback.onPreparedStateChanged(MediaPlayerGlue.this);
448                    }
449                }
450            }
451        });
452
453        if (mOnCompletionListener != null) {
454            mPlayer.setOnCompletionListener(mOnCompletionListener);
455        }
456
457        mPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
458            @Override
459            public void onBufferingUpdate(MediaPlayer mp, int percent) {
460                if (getControlsRow() == null) {
461                    return;
462                }
463                getControlsRow().setBufferedProgress((int) (mp.getDuration() * (percent / 100f)));
464            }
465        });
466        mPlayer.prepareAsync();
467        onStateChanged();
468    }
469
470    /**
471     * This is a listener implementation for the {@link OnItemViewSelectedListener}.
472     * This implementation is required in order to detect KEY_DOWN events
473     * on the {@link androidx.leanback.widget.PlaybackControlsRow.FastForwardAction} and
474     * {@link androidx.leanback.widget.PlaybackControlsRow.RewindAction}. Thus you
475     * should <u>NOT</u> set another {@link OnItemViewSelectedListener} on your
476     * Fragment. Instead, override this method and call its super (this)
477     * implementation.
478     *
479     * @see OnItemViewSelectedListener#onItemSelected(
480     *Presenter.ViewHolder, Object, RowPresenter.ViewHolder, Object)
481     */
482    @Override
483    public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
484                               RowPresenter.ViewHolder rowViewHolder, Row row) {
485        if (item instanceof Action) {
486            mSelectedAction = (Action) item;
487        } else {
488            mSelectedAction = null;
489        }
490    }
491
492    @Override
493    public boolean isPrepared() {
494        return mInitialized;
495    }
496
497    /**
498     * Implements {@link SurfaceHolder.Callback} that can then be set on the
499     * {@link PlaybackGlueHost}.
500     */
501    class VideoPlayerSurfaceHolderCallback implements SurfaceHolder.Callback {
502        @Override
503        public void surfaceCreated(SurfaceHolder surfaceHolder) {
504            setDisplay(surfaceHolder);
505        }
506
507        @Override
508        public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
509        }
510
511        @Override
512        public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
513            setDisplay(null);
514        }
515    }
516}
517