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