VideoView.java revision 5ee0c8832fdfc7a059f52ad4e8b6d2a78f694f3e
1/*
2 * Copyright (C) 2006 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.widget;
18
19import android.app.AlertDialog;
20import android.content.Context;
21import android.content.DialogInterface;
22import android.content.Intent;
23import android.content.res.Resources;
24import android.media.AudioManager;
25import android.media.MediaPlayer;
26import android.media.Metadata;
27import android.media.MediaPlayer.OnCompletionListener;
28import android.media.MediaPlayer.OnErrorListener;
29import android.net.Uri;
30import android.util.AttributeSet;
31import android.util.Log;
32import android.view.KeyEvent;
33import android.view.MotionEvent;
34import android.view.SurfaceHolder;
35import android.view.SurfaceView;
36import android.view.View;
37import android.widget.MediaController.MediaPlayerControl;
38
39import java.io.IOException;
40import java.util.Map;
41
42/**
43 * Displays a video file.  The VideoView class
44 * can load images from various sources (such as resources or content
45 * providers), takes care of computing its measurement from the video so that
46 * it can be used in any layout manager, and provides various display options
47 * such as scaling and tinting.
48 */
49public class VideoView extends SurfaceView implements MediaPlayerControl {
50    private String TAG = "VideoView";
51    // settable by the client
52    private Uri         mUri;
53    private Map<String, String> mHeaders;
54    private int         mDuration;
55
56    // all possible internal states
57    private static final int STATE_ERROR              = -1;
58    private static final int STATE_IDLE               = 0;
59    private static final int STATE_PREPARING          = 1;
60    private static final int STATE_PREPARED           = 2;
61    private static final int STATE_PLAYING            = 3;
62    private static final int STATE_PAUSED             = 4;
63    private static final int STATE_PLAYBACK_COMPLETED = 5;
64
65    // mCurrentState is a VideoView object's current state.
66    // mTargetState is the state that a method caller intends to reach.
67    // For instance, regardless the VideoView object's current state,
68    // calling pause() intends to bring the object to a target state
69    // of STATE_PAUSED.
70    private int mCurrentState = STATE_IDLE;
71    private int mTargetState  = STATE_IDLE;
72
73    // All the stuff we need for playing and showing a video
74    private SurfaceHolder mSurfaceHolder = null;
75    private MediaPlayer mMediaPlayer = null;
76    private int         mVideoWidth;
77    private int         mVideoHeight;
78    private int         mSurfaceWidth;
79    private int         mSurfaceHeight;
80    private MediaController mMediaController;
81    private OnCompletionListener mOnCompletionListener;
82    private MediaPlayer.OnPreparedListener mOnPreparedListener;
83    private int         mCurrentBufferPercentage;
84    private OnErrorListener mOnErrorListener;
85    private int         mSeekWhenPrepared;  // recording the seek position while preparing
86    private boolean     mCanPause;
87    private boolean     mCanSeekBack;
88    private boolean     mCanSeekForward;
89
90    public VideoView(Context context) {
91        super(context);
92        initVideoView();
93    }
94
95    public VideoView(Context context, AttributeSet attrs) {
96        this(context, attrs, 0);
97        initVideoView();
98    }
99
100    public VideoView(Context context, AttributeSet attrs, int defStyle) {
101        super(context, attrs, defStyle);
102        initVideoView();
103    }
104
105    @Override
106    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
107        //Log.i("@@@@", "onMeasure");
108        int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
109        int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
110        if (mVideoWidth > 0 && mVideoHeight > 0) {
111            if ( mVideoWidth * height  > width * mVideoHeight ) {
112                //Log.i("@@@", "image too tall, correcting");
113                height = width * mVideoHeight / mVideoWidth;
114            } else if ( mVideoWidth * height  < width * mVideoHeight ) {
115                //Log.i("@@@", "image too wide, correcting");
116                width = height * mVideoWidth / mVideoHeight;
117            } else {
118                //Log.i("@@@", "aspect ratio is correct: " +
119                        //width+"/"+height+"="+
120                        //mVideoWidth+"/"+mVideoHeight);
121            }
122        }
123        //Log.i("@@@@@@@@@@", "setting size: " + width + 'x' + height);
124        setMeasuredDimension(width, height);
125    }
126
127    public int resolveAdjustedSize(int desiredSize, int measureSpec) {
128        int result = desiredSize;
129        int specMode = MeasureSpec.getMode(measureSpec);
130        int specSize =  MeasureSpec.getSize(measureSpec);
131
132        switch (specMode) {
133            case MeasureSpec.UNSPECIFIED:
134                /* Parent says we can be as big as we want. Just don't be larger
135                 * than max size imposed on ourselves.
136                 */
137                result = desiredSize;
138                break;
139
140            case MeasureSpec.AT_MOST:
141                /* Parent says we can be as big as we want, up to specSize.
142                 * Don't be larger than specSize, and don't be larger than
143                 * the max size imposed on ourselves.
144                 */
145                result = Math.min(desiredSize, specSize);
146                break;
147
148            case MeasureSpec.EXACTLY:
149                // No choice. Do what we are told.
150                result = specSize;
151                break;
152        }
153        return result;
154}
155
156    private void initVideoView() {
157        mVideoWidth = 0;
158        mVideoHeight = 0;
159        getHolder().addCallback(mSHCallback);
160        getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
161        setFocusable(true);
162        setFocusableInTouchMode(true);
163        requestFocus();
164        mCurrentState = STATE_IDLE;
165        mTargetState  = STATE_IDLE;
166    }
167
168    public void setVideoPath(String path) {
169        setVideoURI(Uri.parse(path));
170    }
171
172    public void setVideoURI(Uri uri) {
173        setVideoURI(uri, null);
174    }
175
176    /**
177     * @hide
178     */
179    public void setVideoURI(Uri uri, Map<String, String> headers) {
180        mUri = uri;
181        mHeaders = headers;
182        mSeekWhenPrepared = 0;
183        openVideo();
184        requestLayout();
185        invalidate();
186    }
187
188    public void stopPlayback() {
189        if (mMediaPlayer != null) {
190            mMediaPlayer.stop();
191            mMediaPlayer.release();
192            mMediaPlayer = null;
193            mCurrentState = STATE_IDLE;
194            mTargetState  = STATE_IDLE;
195        }
196    }
197
198    private void openVideo() {
199        if (mUri == null || mSurfaceHolder == null) {
200            // not ready for playback just yet, will try again later
201            return;
202        }
203        // Tell the music playback service to pause
204        // TODO: these constants need to be published somewhere in the framework.
205        Intent i = new Intent("com.android.music.musicservicecommand");
206        i.putExtra("command", "pause");
207        mContext.sendBroadcast(i);
208
209        // we shouldn't clear the target state, because somebody might have
210        // called start() previously
211        release(false);
212        try {
213            mMediaPlayer = new MediaPlayer();
214            mMediaPlayer.setOnPreparedListener(mPreparedListener);
215            mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
216            mDuration = -1;
217            mMediaPlayer.setOnCompletionListener(mCompletionListener);
218            mMediaPlayer.setOnErrorListener(mErrorListener);
219            mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
220            mCurrentBufferPercentage = 0;
221            mMediaPlayer.setDataSource(mContext, mUri, mHeaders);
222            mMediaPlayer.setDisplay(mSurfaceHolder);
223            mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
224            mMediaPlayer.setScreenOnWhilePlaying(true);
225            mMediaPlayer.prepareAsync();
226            // we don't set the target state here either, but preserve the
227            // target state that was there before.
228            mCurrentState = STATE_PREPARING;
229            attachMediaController();
230        } catch (IOException ex) {
231            Log.w(TAG, "Unable to open content: " + mUri, ex);
232            mCurrentState = STATE_ERROR;
233            mTargetState = STATE_ERROR;
234            mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
235            return;
236        } catch (IllegalArgumentException ex) {
237            Log.w(TAG, "Unable to open content: " + mUri, ex);
238            mCurrentState = STATE_ERROR;
239            mTargetState = STATE_ERROR;
240            mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
241            return;
242        }
243    }
244
245    public void setMediaController(MediaController controller) {
246        if (mMediaController != null) {
247            mMediaController.hide();
248        }
249        mMediaController = controller;
250        attachMediaController();
251    }
252
253    private void attachMediaController() {
254        if (mMediaPlayer != null && mMediaController != null) {
255            mMediaController.setMediaPlayer(this);
256            View anchorView = this.getParent() instanceof View ?
257                    (View)this.getParent() : this;
258            mMediaController.setAnchorView(anchorView);
259            mMediaController.setEnabled(isInPlaybackState());
260        }
261    }
262
263    MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
264        new MediaPlayer.OnVideoSizeChangedListener() {
265            public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
266                mVideoWidth = mp.getVideoWidth();
267                mVideoHeight = mp.getVideoHeight();
268                if (mVideoWidth != 0 && mVideoHeight != 0) {
269                    getHolder().setFixedSize(mVideoWidth, mVideoHeight);
270                }
271            }
272    };
273
274    MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {
275        public void onPrepared(MediaPlayer mp) {
276            mCurrentState = STATE_PREPARED;
277
278            // Get the capabilities of the player for this stream
279            Metadata data = mp.getMetadata(MediaPlayer.METADATA_ALL,
280                                      MediaPlayer.BYPASS_METADATA_FILTER);
281
282            if (data != null) {
283                mCanPause = !data.has(Metadata.PAUSE_AVAILABLE)
284                        || data.getBoolean(Metadata.PAUSE_AVAILABLE);
285                mCanSeekBack = !data.has(Metadata.SEEK_BACKWARD_AVAILABLE)
286                        || data.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE);
287                mCanSeekForward = !data.has(Metadata.SEEK_FORWARD_AVAILABLE)
288                        || data.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE);
289            } else {
290                mCanPause = mCanSeekBack = mCanSeekForward = true;
291            }
292
293            if (mOnPreparedListener != null) {
294                mOnPreparedListener.onPrepared(mMediaPlayer);
295            }
296            if (mMediaController != null) {
297                mMediaController.setEnabled(true);
298            }
299            mVideoWidth = mp.getVideoWidth();
300            mVideoHeight = mp.getVideoHeight();
301
302            int seekToPosition = mSeekWhenPrepared;  // mSeekWhenPrepared may be changed after seekTo() call
303            if (seekToPosition != 0) {
304                seekTo(seekToPosition);
305            }
306            if (mVideoWidth != 0 && mVideoHeight != 0) {
307                //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight);
308                getHolder().setFixedSize(mVideoWidth, mVideoHeight);
309                if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) {
310                    // We didn't actually change the size (it was already at the size
311                    // we need), so we won't get a "surface changed" callback, so
312                    // start the video here instead of in the callback.
313                    if (mTargetState == STATE_PLAYING) {
314                        start();
315                        if (mMediaController != null) {
316                            mMediaController.show();
317                        }
318                    } else if (!isPlaying() &&
319                               (seekToPosition != 0 || getCurrentPosition() > 0)) {
320                       if (mMediaController != null) {
321                           // Show the media controls when we're paused into a video and make 'em stick.
322                           mMediaController.show(0);
323                       }
324                   }
325                }
326            } else {
327                // We don't know the video size yet, but should start anyway.
328                // The video size might be reported to us later.
329                if (mTargetState == STATE_PLAYING) {
330                    start();
331                }
332            }
333        }
334    };
335
336    private MediaPlayer.OnCompletionListener mCompletionListener =
337        new MediaPlayer.OnCompletionListener() {
338        public void onCompletion(MediaPlayer mp) {
339            mCurrentState = STATE_PLAYBACK_COMPLETED;
340            mTargetState = STATE_PLAYBACK_COMPLETED;
341            if (mMediaController != null) {
342                mMediaController.hide();
343            }
344            if (mOnCompletionListener != null) {
345                mOnCompletionListener.onCompletion(mMediaPlayer);
346            }
347        }
348    };
349
350    private MediaPlayer.OnErrorListener mErrorListener =
351        new MediaPlayer.OnErrorListener() {
352        public boolean onError(MediaPlayer mp, int framework_err, int impl_err) {
353            Log.d(TAG, "Error: " + framework_err + "," + impl_err);
354            mCurrentState = STATE_ERROR;
355            mTargetState = STATE_ERROR;
356            if (mMediaController != null) {
357                mMediaController.hide();
358            }
359
360            /* If an error handler has been supplied, use it and finish. */
361            if (mOnErrorListener != null) {
362                if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err)) {
363                    return true;
364                }
365            }
366
367            /* Otherwise, pop up an error dialog so the user knows that
368             * something bad has happened. Only try and pop up the dialog
369             * if we're attached to a window. When we're going away and no
370             * longer have a window, don't bother showing the user an error.
371             */
372            if (getWindowToken() != null) {
373                Resources r = mContext.getResources();
374                int messageId;
375
376                if (framework_err == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
377                    messageId = com.android.internal.R.string.VideoView_error_text_invalid_progressive_playback;
378                } else {
379                    messageId = com.android.internal.R.string.VideoView_error_text_unknown;
380                }
381
382                new AlertDialog.Builder(mContext)
383                        .setTitle(com.android.internal.R.string.VideoView_error_title)
384                        .setMessage(messageId)
385                        .setPositiveButton(com.android.internal.R.string.VideoView_error_button,
386                                new DialogInterface.OnClickListener() {
387                                    public void onClick(DialogInterface dialog, int whichButton) {
388                                        /* If we get here, there is no onError listener, so
389                                         * at least inform them that the video is over.
390                                         */
391                                        if (mOnCompletionListener != null) {
392                                            mOnCompletionListener.onCompletion(mMediaPlayer);
393                                        }
394                                    }
395                                })
396                        .setCancelable(false)
397                        .show();
398            }
399            return true;
400        }
401    };
402
403    private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener =
404        new MediaPlayer.OnBufferingUpdateListener() {
405        public void onBufferingUpdate(MediaPlayer mp, int percent) {
406            mCurrentBufferPercentage = percent;
407        }
408    };
409
410    /**
411     * Register a callback to be invoked when the media file
412     * is loaded and ready to go.
413     *
414     * @param l The callback that will be run
415     */
416    public void setOnPreparedListener(MediaPlayer.OnPreparedListener l)
417    {
418        mOnPreparedListener = l;
419    }
420
421    /**
422     * Register a callback to be invoked when the end of a media file
423     * has been reached during playback.
424     *
425     * @param l The callback that will be run
426     */
427    public void setOnCompletionListener(OnCompletionListener l)
428    {
429        mOnCompletionListener = l;
430    }
431
432    /**
433     * Register a callback to be invoked when an error occurs
434     * during playback or setup.  If no listener is specified,
435     * or if the listener returned false, VideoView will inform
436     * the user of any errors.
437     *
438     * @param l The callback that will be run
439     */
440    public void setOnErrorListener(OnErrorListener l)
441    {
442        mOnErrorListener = l;
443    }
444
445    SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback()
446    {
447        public void surfaceChanged(SurfaceHolder holder, int format,
448                                    int w, int h)
449        {
450            mSurfaceWidth = w;
451            mSurfaceHeight = h;
452            boolean isValidState =  (mTargetState == STATE_PLAYING);
453            boolean hasValidSize = (mVideoWidth == w && mVideoHeight == h);
454            if (mMediaPlayer != null && isValidState && hasValidSize) {
455                if (mSeekWhenPrepared != 0) {
456                    seekTo(mSeekWhenPrepared);
457                }
458                start();
459                if (mMediaController != null) {
460                    if (mMediaController.isShowing()) {
461                        // ensure the controller will get repositioned later
462                        mMediaController.hide();
463                    }
464                    mMediaController.show();
465                }
466            }
467        }
468
469        public void surfaceCreated(SurfaceHolder holder)
470        {
471            mSurfaceHolder = holder;
472            openVideo();
473        }
474
475        public void surfaceDestroyed(SurfaceHolder holder)
476        {
477            // after we return from this we can't use the surface any more
478            mSurfaceHolder = null;
479            if (mMediaController != null) mMediaController.hide();
480            release(true);
481        }
482    };
483
484    /*
485     * release the media player in any state
486     */
487    private void release(boolean cleartargetstate) {
488        if (mMediaPlayer != null) {
489            mMediaPlayer.reset();
490            mMediaPlayer.release();
491            mMediaPlayer = null;
492            mCurrentState = STATE_IDLE;
493            if (cleartargetstate) {
494                mTargetState  = STATE_IDLE;
495            }
496        }
497    }
498
499    @Override
500    public boolean onTouchEvent(MotionEvent ev) {
501        if (isInPlaybackState() && mMediaController != null) {
502            toggleMediaControlsVisiblity();
503        }
504        return false;
505    }
506
507    @Override
508    public boolean onTrackballEvent(MotionEvent ev) {
509        if (isInPlaybackState() && mMediaController != null) {
510            toggleMediaControlsVisiblity();
511        }
512        return false;
513    }
514
515    @Override
516    public boolean onKeyDown(int keyCode, KeyEvent event)
517    {
518        boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK &&
519                                     keyCode != KeyEvent.KEYCODE_VOLUME_UP &&
520                                     keyCode != KeyEvent.KEYCODE_VOLUME_DOWN &&
521                                     keyCode != KeyEvent.KEYCODE_VOLUME_MUTE &&
522                                     keyCode != KeyEvent.KEYCODE_MENU &&
523                                     keyCode != KeyEvent.KEYCODE_CALL &&
524                                     keyCode != KeyEvent.KEYCODE_ENDCALL;
525        if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) {
526            if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
527                    keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
528                if (mMediaPlayer.isPlaying()) {
529                    pause();
530                    mMediaController.show();
531                } else {
532                    start();
533                    mMediaController.hide();
534                }
535                return true;
536            } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
537                if (mMediaPlayer.isPlaying()) {
538                    start();
539                    mMediaController.hide();
540                }
541                return true;
542            } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
543                    || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
544                if (!mMediaPlayer.isPlaying()) {
545                    pause();
546                    mMediaController.show();
547                }
548                return true;
549            } else {
550                toggleMediaControlsVisiblity();
551            }
552        }
553
554        return super.onKeyDown(keyCode, event);
555    }
556
557    private void toggleMediaControlsVisiblity() {
558        if (mMediaController.isShowing()) {
559            mMediaController.hide();
560        } else {
561            mMediaController.show();
562        }
563    }
564
565    public void start() {
566        if (isInPlaybackState()) {
567            mMediaPlayer.start();
568            mCurrentState = STATE_PLAYING;
569        }
570        mTargetState = STATE_PLAYING;
571    }
572
573    public void pause() {
574        if (isInPlaybackState()) {
575            if (mMediaPlayer.isPlaying()) {
576                mMediaPlayer.pause();
577                mCurrentState = STATE_PAUSED;
578            }
579        }
580        mTargetState = STATE_PAUSED;
581    }
582
583    public void suspend() {
584        release(false);
585    }
586
587    public void resume() {
588        openVideo();
589    }
590
591    // cache duration as mDuration for faster access
592    public int getDuration() {
593        if (isInPlaybackState()) {
594            if (mDuration > 0) {
595                return mDuration;
596            }
597            mDuration = mMediaPlayer.getDuration();
598            return mDuration;
599        }
600        mDuration = -1;
601        return mDuration;
602    }
603
604    public int getCurrentPosition() {
605        if (isInPlaybackState()) {
606            return mMediaPlayer.getCurrentPosition();
607        }
608        return 0;
609    }
610
611    public void seekTo(int msec) {
612        if (isInPlaybackState()) {
613            mMediaPlayer.seekTo(msec);
614            mSeekWhenPrepared = 0;
615        } else {
616            mSeekWhenPrepared = msec;
617        }
618    }
619
620    public boolean isPlaying() {
621        return isInPlaybackState() && mMediaPlayer.isPlaying();
622    }
623
624    public int getBufferPercentage() {
625        if (mMediaPlayer != null) {
626            return mCurrentBufferPercentage;
627        }
628        return 0;
629    }
630
631    private boolean isInPlaybackState() {
632        return (mMediaPlayer != null &&
633                mCurrentState != STATE_ERROR &&
634                mCurrentState != STATE_IDLE &&
635                mCurrentState != STATE_PREPARING);
636    }
637
638    public boolean canPause() {
639        return mCanPause;
640    }
641
642    public boolean canSeekBackward() {
643        return mCanSeekBack;
644    }
645
646    public boolean canSeekForward() {
647        return mCanSeekForward;
648    }
649}
650