I had a very hard time implementing a player supporting many features and formats. Even VideoView didn't match all of my needs. Finally, I ended up writting my own solution based on SurfaceView and MediaPlayer. Here is the source code. It was validated on 4.0.3 and 4.1.2 for local 3gp, local mp4, http 3gp and youtube rtsp streams.
Remember that, presently, an URI for valid youtube video on android is to be like this: rtsp://v5.cache1.c.youtube.com/CjYLENy73wIaLQnhycnrJQ8qmRMYESARFEIJbXYtZ29vZ2xlSARSBXdhdGNoYPj_hYjnq6uUTQw=/0/0/0/video.3gp
. You get them via http://m.youtube.video.
The activity's arguments are set by Intent. There are quite a variety of options. See the last source code provided in my comment. You can remove the code related to Sequencer, Scenario and Test classes. They are specific to my program (automated system for platform testing)
package my.package;
import java.io.IOException;
import java.lang.ref.WeakReference;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Point;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnBufferingUpdateListener;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.media.MediaPlayer.OnVideoSizeChangedListener;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.text.format.Time;
import android.view.Display;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import my.package.Log;
import my.package.R;
import my.package.Scenario;
import my.package.Sequencer;
import my.package.Test;
import my.package.TestUtil;
/**
* An activity to display local or remote video or play audio file.
*/
public class MediaPlayerActivity extends Activity implements
OnBufferingUpdateListener, OnCompletionListener, OnPreparedListener,
OnVideoSizeChangedListener, OnErrorListener, SurfaceHolder.Callback {
private static final String LOG_TAG = MediaPlayerActivity.class
.getSimpleName();
// Mandatory creation arguments passed by extras in starting Intent
public static final String SEQUENCER = "sequencer"; //$NON-NLS-1$
public static final String SCENARIO = "scenario"; //$NON-NLS-1$
public static final String TEST = "test"; //$NON-NLS-1$
public static final String SOURCE_URI = "uri"; //$NON-NLS-1$
// Optional creation arguments passed by extras in starting Intent
public static final String PAUSE_ON_BACKGROUND = "auto_pause"; //$NON-NLS-1$
public static final String TIMEOUT = "timeout"; //$NON-NLS-1$
public static final String LOOP = "loop"; //$NON-NLS-1$
public static final String SCREEN_ON_WHILE_PLAYING = "screen_on_while_playing"; //$NON-NLS-1$
// data arguments returned by Intent on finish
public static final String REASON = "cause"; //$NON-NLS-1$
public static final String EXCEPTION = "exception"; //$NON-NLS-1$
// additional state bundle arguments.
private static final String START_POSITION = "start"; //$NON-NLS-1$
private static final String VIDEO_WIDTH = "video_width"; //$NON-NLS-1$
private static final String VIDEO_HEIGHT = "video_height"; //$NON-NLS-1$
private WeakReference<Sequencer> mSequencer = new WeakReference<Sequencer> (null);
private WeakReference<Test> mTest = new WeakReference<Test> (null);
/**
* URI of the video/audio source.
*
* This player supports a variety of videos and audio sources, either local
* or remote.
* <p>
* An HTTP live streaming URI would be:
* {@code httplive://xboodangx.api.channel.livestream.com/3.0/playlist.m3u8}
* </p>
* <p>
* A local video file URI would be {@code file:///sdcard/spiderman.mp4}
* </p>
* <p>
* A remote 3GPP format video URI would be
* {@code http://commonsware.com/misc/test2.3gp}
* </p>
* <p>
* And finally an RTP or RTSP video source URI would be
* {@code rtsp://v4.cache1.c.youtube.com/CjYLENy73wIaLQk4RDShYkdS1BMYDSANFEIJbXYtZ29vZ2xlSARSBXdhdGNoYK-Cn8qh8J6-Tgw=/0/0/0/video.3gp}
* </p>
*/
private Uri mMediaURI;
/**
* Input: this flag is set to true if the video must be paused when the
* activity is not visible any more. The video will resume when the activity
* is visible again.
*/
private boolean mPauseOnBackground;
/**
* Input: number of milliseconds until automatic shutdown, or -1 if no
* timeout.
*/
private long mTimeout;
/**
* Input: flag set to true to loop back to the beginning of the video when
* reaching its end
*/
private boolean mLoop;
/**
* The width of the video, obtained by
* {@link #onVideoSizeChanged(MediaPlayer, int, int)}
*/
private int mVideoWidth;
/**
* The height of the video, obtained by
* {@link #onVideoSizeChanged(MediaPlayer, int, int)}
*/
private int mVideoHeight;
private MediaPlayer mMediaPlayer;
private SurfaceView mPreview;
private boolean mIsVideoSizeKnown = false;
private boolean mMediaPrepared = false;
/**
* This member is set to position the video should start. It is set when
* pausing the video and used when restoring the instance.
*/
private int mStartPosition;
private boolean mTimeoutSet;
private boolean mScreenOnWhilePlaying;
private SurfaceHolder mHolder;
private static class ShutdownHandler extends Handler {
WeakReference<MediaPlayerActivity> mActivity;
public ShutdownHandler(MediaPlayerActivity activity) {
mActivity = new WeakReference<MediaPlayerActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
MediaPlayerActivity activity = mActivity.get();
if (activity != null) {
activity.finishTest(Activity.RESULT_OK, null);
} else {
//Log.w(LOG_TAG, "no activity for shutdown");
}
}
}
public MediaPlayerActivity() {
super();
// These members are initialized in onCreate(Bundle) by the
// starting Intent, the first time the activity is created, and restored
// in the same method with the arguments in the saved instance bundle.
mSequencer = new WeakReference<Sequencer> (null);
mTest = new WeakReference<Test> (null);
setSourceURI(null);
setPauseOnBackground(false);
setPlayTimeout(-1);
setLooping(false);
setScreenOnWhilePlaying(true);
// Initialize internals.
mIsVideoSizeKnown = false;
mVideoWidth = mVideoHeight = 0; // unknown
mMediaPrepared = false;
mMediaPlayer = null;
mPreview = null; // set in onCreate(Bundle)
setStartPosition(0); // beginning of the video
mTimeoutSet = false;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.video_player);
Intent intent = getIntent();
if (savedInstanceState != null) {
onRestoreInstanceState(savedInstanceState);
} else if (intent != null) {
Log.d(LOG_TAG, "Loading starting Intent extras...");
// read starting Intent extras.
_updateForeignReferences(intent);
setSourceURI((Uri) intent.getParcelableExtra(SOURCE_URI));
setPlayTimeout(intent.getLongExtra(TIMEOUT, -1L));
setPauseOnBackground(intent.getBooleanExtra(PAUSE_ON_BACKGROUND,
false));
setLooping(intent.getBooleanExtra(LOOP, false));
setScreenOnWhilePlaying(intent.getBooleanExtra(SCREEN_ON_WHILE_PLAYING, true));
}
mTimeoutSet = false;
_updateWidgets();
}
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
Log.d(LOG_TAG, "Restoring instance state...");
// restore saved references
_updateSavedForeignReferences(savedInstanceState);
// restore saved inputs
setSourceURI(Uri.parse(savedInstanceState.getString(SOURCE_URI)));
setPlayTimeout(savedInstanceState.getLong(TIMEOUT));
setPauseOnBackground(savedInstanceState.getBoolean(PAUSE_ON_BACKGROUND));
setLooping(savedInstanceState.getBoolean(LOOP));
setScreenOnWhilePlaying(savedInstanceState.getBoolean(SCREEN_ON_WHILE_PLAYING));
// restore internals
setStartPosition(savedInstanceState.getInt(START_POSITION, 0));
mVideoWidth = savedInstanceState.getInt(VIDEO_WIDTH);
mVideoHeight = savedInstanceState.getInt(VIDEO_HEIGHT); // unknown
mIsVideoSizeKnown = (mVideoWidth > 0) && (mVideoHeight > 0);
}
@Override
protected void onResume() {
super.onResume();
if (mMediaPlayer == null) {
try {
_playMedia();
} catch (Exception e) {
_fail(e);
}
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Log.d(LOG_TAG, "Saving instance state");
Sequencer sequencer = mSequencer.get();
if (sequencer != null) {
outState.putInt(SEQUENCER, sequencer.getPosition());
Scenario scenario = sequencer.getScenario();
if (scenario != null) {
outState.putInt(SCENARIO, scenario.getScenarioId());
}
}
Test test = mTest.get();
if (test != null) {
outState.putString(TEST, TestUtil.getTestPath(test, TestUtil.Path.Static));
}
if (getSourceURI() != null) {
outState.putString(SOURCE_URI, getSourceURI().toString());
}
outState.putBoolean(LOOP, isLooping());
outState.putBoolean(PAUSE_ON_BACKGROUND, isPausingOnBackground());
outState.putLong(TIMEOUT, getPlayTimeout());
outState.putBoolean(SCREEN_ON_WHILE_PLAYING, isScreenOnWhilePlaying());
outState.putInt(START_POSITION, getStartPosition());
}
@Override
protected void onPause() {
super.onPause();
if (isPausingOnBackground()) {
_pausePlayback();
}
}
@Override
protected void onStop() {
super.onStop();
Log.d(LOG_TAG, "onStop");
if (isPausingOnBackground()) {
_releaseMediaPlayer();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(LOG_TAG, "onDestroy");
// TODO: It would be fine to fail the test if the activity was destroyed
// if we didn't finished the test yet but there are far too many cases
// and I failed to implement one working with all of them!
/* if (!mInstanceSaved) {
finishTest(Activity.RESULT_FIRST_USER, new Intent().putExtra(REASON,
"Activity destroyed. Something certainly goes wrong there."));
}
*/
_releaseMediaPlayer();
}
// Restore sequencer, scenario and test references from saved state.
private void _updateSavedForeignReferences(Bundle savedInstanceState) {
int sequencer = savedInstanceState.getInt(SEQUENCER, -1);
int scenarioId = savedInstanceState.getInt(SCENARIO, -1);
mSequencer = new WeakReference<Sequencer>(null);
mTest = new WeakReference<Test>(null);
if (scenarioId >= 0 && sequencer >= 0) {
Scenario scenario = Controller.controller.getData().scenarios()
.getScenario(scenarioId);
mSequencer = new WeakReference<Sequencer>(Controller.controller
.engine().getSequencerAt(sequencer));
String testPath = savedInstanceState.getString(TEST);
if (!TextUtils.isEmpty(testPath)) {
mTest = new WeakReference<Test>(TestUtil.fromPath(
scenario.getRootTest(), testPath));
}
}
}
// Update sequencer, scenario and test references from starting Intent
protected void _updateForeignReferences(Intent intent) {
int scenarioId = intent.getIntExtra(MediaPlayerActivity.SCENARIO, -1);
Scenario scenario = Controller.controller.getData().scenarios()
.getScenario(scenarioId);
int sequencer = intent.getIntExtra(MediaPlayerActivity.SEQUENCER, -1);
mSequencer = new WeakReference<Sequencer>(null);
mTest = new WeakReference<Test>(null);
if (scenarioId >= 0 && sequencer >= 0) {
mSequencer = new WeakReference<Sequencer>(Controller.controller
.engine().getSequencerAt(sequencer));
String testPath = intent.getStringExtra(MediaPlayerActivity.TEST);
if (!TextUtils.isEmpty(testPath)) {
mTest = new WeakReference<Test>(TestUtil.fromPath(
scenario.getRootTest(), testPath));
}
}
}
/**
* Terminate the test case and finish the activity at the same time.
* <p>
* The result code and data are passed to both the parent activity and the
* {@link Test#terminate(int, Sequencer, Object)} method.
* </p>
*
* @param resultCode
* the result code. May be on of the Activity result code but
* also any other one having a meaning for the test and caller
* activity.
* @param data
* extra result data. Can be null.
*/
public void finishTest(int resultCode, Intent data) {
Test test = mTest.get();
Sequencer sequencer = mSequencer.get();
if ((test != null) && (sequencer != null)) {
test.terminate(resultCode, sequencer, data);
// prevent any further call to finishTest.
mTest = new WeakReference<Test> (null);
}
if (!isFinishing()) {
setResult(Activity.RESULT_OK, data);
finish();
}
}
@SuppressWarnings("deprecation")
private void _updateWidgets() {
SurfaceView surface = (SurfaceView) findViewById(R.id.surface);
mPreview = surface;
SurfaceHolder holder = surface.getHolder();
holder.addCallback(this);
if (VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) {
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
}
public void onBufferingUpdate(MediaPlayer arg0, int percent) {
Log.d(LOG_TAG, "onBufferingUpdate percent:" + percent);
if (percent >= 100) {
mMediaPlayer.setOnBufferingUpdateListener(null);
}
// no display so don't bother.
}
public void onCompletion(MediaPlayer player) {
_releaseMediaPlayer();
finishTest(Activity.RESULT_OK, null);
}
private void _rearmPlayer() {
mVideoWidth = 0;
mVideoHeight = 0;
mMediaPrepared = false;
mIsVideoSizeKnown = false;
}
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
if (mIsVideoSizeKnown) return; // further notifications are completely wrong!!
Log.i(LOG_TAG, "Video size: " + width + "x" + height);
mIsVideoSizeKnown = true;
mVideoWidth = width;
mVideoHeight = height;
if (width > 0 && height > 0) {
_fitSurfaceForVideo();
} else {
// audio only or video size unknown.
}
if (mMediaPrepared) {
_startPlayback();
}
}
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
private void _fitSurfaceForVideo() {
Display display = getWindowManager().getDefaultDisplay();
Point size = new Point();
if (VERSION.SDK_INT >= 13) {
display.getSize(size);
} else {
size.x = display.getWidth();
size.y = display.getHeight();
}
double ratioVideo = (double)mVideoWidth / mVideoHeight;
double ratioScreen = (double)size.x / size.y;
if (ratioScreen > ratioVideo) {
// fit in height and scale the width to match the video ratio
mVideoHeight = size.y;
mVideoWidth = (int) (size.y / ratioVideo);
} else {
// fit in width and scale height to keep the video ratio
mVideoWidth = size.x;
mVideoHeight = (int) (size.x / ratioVideo);
}
}
public void onPrepared(MediaPlayer mediaplayer) {
mMediaPrepared = true;
if (mIsVideoSizeKnown) {
_startPlayback();
}
}
private void _startPlayback() {
SurfaceHolder holder = mPreview.getHolder();
if (mVideoWidth > 0 && mVideoHeight > 0) {
holder.setFixedSize(mVideoWidth, mVideoHeight);
}
if (mStartPosition > 0) {
Log.i(LOG_TAG, String.format("Resume at %f seconds", mStartPosition / 1000.f));
mMediaPlayer.seekTo(mStartPosition);
} else {
Log.i(LOG_TAG, "Start video");
}
if (!mTimeoutSet && (getPlayTimeout() > 0)) {
// this is a constant time reference: deduce the time already played.
long remaining = getPlayTimeout() - mStartPosition;
Time time = new Time();
time.setToNow();
time.second += remaining / 1000;
time.normalize(false);
Log.i(LOG_TAG, "Will end the video on " + time.format2445());
new ShutdownHandler(this).sendEmptyMessageDelayed(0, remaining);
mTimeoutSet = true;
}
mMediaPlayer.start();
}
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
Log.d(LOG_TAG, String.format(
"surfaceChanged called: format=%d, width=%d, height=%d",
format, width, height));
}
public void surfaceDestroyed(SurfaceHolder surfaceholder) {
Log.d(LOG_TAG, "surfaceDestroyed called");
mHolder = null;
if (mMediaPlayer != null) {
mMediaPlayer.setDisplay(null);
}
}
public void surfaceCreated(SurfaceHolder holder) {
Log.d(LOG_TAG, "surfaceCreated called");
mHolder = holder;
if (mMediaPlayer != null) {
// surface created after the media player
try {
mMediaPlayer.setDisplay(holder);
mMediaPlayer.setScreenOnWhilePlaying(isScreenOnWhilePlaying());
mMediaPlayer.setOnCompletionListener(this);
if (!mMediaPrepared) {
mMediaPlayer.setDataSource(this, mMediaURI);
mMediaPlayer.prepareAsync();
}
} catch (IllegalStateException e) {
_fail(e);
} catch (IllegalArgumentException e) {
_fail(e);
} catch (SecurityException e) {
_fail(e);
} catch (IOException e) {
_fail(e);
}
}
}
private void _playMedia() throws IllegalArgumentException,
SecurityException, IllegalStateException, IOException {
Log.d(LOG_TAG, "_playMedia()");
_rearmPlayer();
/*
* The video should be in a streamable mp4 or 3gpp format. The URI
* scheme may be Http. Mediaplayer can only play
* "progressive streamable contents" which basically means: 1. the movie
* atom has to precede all the media data atoms. 2. The clip has to be
* reasonably interleaved.
*/
if (mMediaURI != null) {
Log.i(LOG_TAG, "Source: " + mMediaURI);
// Create a new media player and set the listeners
if (mMediaPlayer != null) {
mMediaPlayer.reset();
} else {
mMediaPlayer = new MediaPlayer();
}
// mMediaPlayer.setDataSource(this, mMediaURI);
mMediaPlayer.setOnBufferingUpdateListener(this);
mMediaPlayer.setOnPreparedListener(this);
mMediaPlayer.setLooping(isLooping());
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setOnVideoSizeChangedListener(this);
if (mHolder != null) {
// surface created before the media player
mMediaPlayer.setDisplay(mHolder);
mMediaPlayer.setScreenOnWhilePlaying(isScreenOnWhilePlaying());
mMediaPlayer.setOnCompletionListener(this);
mMediaPlayer.setDataSource(this, mMediaURI);
mMediaPlayer.prepareAsync();
}
} else {
_fail("No video source defined");
}
}
// fail due to exception
private void _fail(Exception e) {
Log.e(LOG_TAG, e);
Intent data = new Intent();
data.putExtra(EXCEPTION, e);
finishTest(Activity.RESULT_FIRST_USER, data);
}
// fail due to generic error
private void _fail(String text) {
Log.e(LOG_TAG, text);
Intent data = new Intent();
data.putExtra(REASON, text);
finishTest(Activity.RESULT_FIRST_USER, data);
}
private void _pausePlayback() {
if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
Log.i(LOG_TAG, "Pause playback");
mMediaPlayer.pause();
// retain position for use by onSaveInstanceState(Bundle)
setStartPosition(mMediaPlayer.getCurrentPosition());
}
}
@Override
public void onBackPressed() {
super.onBackPressed();
Intent data = new Intent();
data.putExtra(REASON, "canceled by user");
finishTest(Activity.RESULT_CANCELED, data);
}
private void _releaseMediaPlayer() {
_stopPlayback();
if (mMediaPlayer != null) {
mMediaPlayer.release();
mMediaPlayer = null;
}
_rearmPlayer();
}
private void _stopPlayback() {
if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
_pausePlayback();
Log.i(LOG_TAG, "stop video");
mMediaPlayer.stop();
}
}
public boolean isPausingOnBackground() {
return mPauseOnBackground;
}
public void setPauseOnBackground(boolean pause) {
Log.d(LOG_TAG, "Pausing on background: " + pause);
this.mPauseOnBackground = pause;
}
public Uri getSourceURI() {
return mMediaURI;
}
public void setSourceURI(Uri uri) {
Log.d(LOG_TAG, "Media source: " + uri);
this.mMediaURI = uri;
}
public long getPlayTimeout() {
return mTimeout;
}
public void setPlayTimeout(long timeout) {
Log.d(LOG_TAG, "Play length (ms): " + timeout);
this.mTimeout = timeout;
}
public boolean isLooping() {
return mLoop;
}
public void setLooping(boolean loop) {
Log.d(LOG_TAG, "Is looping: " + loop);
this.mLoop = loop;
}
public int getStartPosition() {
int position = 0;
if (mMediaPlayer != null) {
position = mMediaPlayer.getCurrentPosition();
}
return position;
}
public void setStartPosition(int position) {
Log.d(LOG_TAG, String.format("Start at %fs", position / 1000.));
this.mStartPosition = position;
}
public boolean isScreenOnWhilePlaying() {
return mScreenOnWhilePlaying;
}
public void setScreenOnWhilePlaying(boolean screenOnWhilePlaying) {
Log.d(LOG_TAG, "Screen ON while playing: " + screenOnWhilePlaying);
this.mScreenOnWhilePlaying = screenOnWhilePlaying;
}
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
_fail(String.format("Media player error: what=%d, extra=%d", what, extra));
return true;
}
}
The resource for the layout containing the SurfaceView:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<SurfaceView
android:id="@+id/surface"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
An example of use of the activity:
Intent intent = new Intent(parent, MediaPlayerActivity.class);
intent.putExtra(MediaPlayerActivity.SOURCE_URI, this.getSourceURI());
intent.putExtra(MediaPlayerActivity.PAUSE_ON_BACKGROUND,
this.getPauseOnBackground());
Scenario scenario = sequencer.getScenario();
intent.putExtra(MediaPlayerActivity.SCENARIO, scenario.getScenarioId());
intent.putExtra(MediaPlayerActivity.SEQUENCER, sequencer.getPosition());
intent.putExtra(MediaPlayerActivity.TEST, TestUtil.getTestPath(
scenario.getCurrentTest(), TestUtil.Path.Static));
// timeout option
TimeSpan timeout = this.getTimeout();
if (!timeout.isNull()) {
timeout.renew();
intent.putExtra(MediaPlayerActivity.TIMEOUT, timeout.value());
}
intent.putExtra(MediaPlayerActivity.LOOP, this.isLooping());
startActivity(intent);