用手机看视频已经是家常便饭,但自己动手开发一个视频播放器却让不少Android开发者头疼。其实,掌握正确的方法后,你会发现它比你想象中简单得多。
在Android系统中,播放视频主要有两种方式:一是使用系统自带的播放器,二是使用VideoView,但效果最好的还是SurfaceView+MediaPlayer的组合。
那么,为什么需要SurfaceView和MediaPlayer两者合作呢?
这就像看电视一样,SurfaceView是电视机屏幕,负责显示画面;MediaPlayer则是DVD播放机,负责解码和播放视频流。
单独的MediaPlayer只能播放音频文件,要想播放视频还需要SurfaceView来配合显示画面。
SurfaceView从Android 1.0就有了,它有一个独特的优势:可以在非UI线程中完成刷新。这样一来,在播放视频时就不需要自己去写handler来实现两个线程之间的通信了。
这种显示与控制分离的机制使得SurfaceView只负责显示画面,而不负责控制视频流,因此还需要SurfaceHolder来控制视频流。
想要熟练使用MediaPlayer,首先必须理解它的生命周期。MediaPlayer有一套非常明确的状态转换规则,了解这些状态对于正确使用MediaPlayer至关重要。
从MediaPlayer的生命周期图,可以看出使用MediaPlayer不是很复杂。
最简单的流程:
初始化->reset()->setDataSource()->prepare()->start()->……只要简单的几步,就可以播放音频文件了。
当创建MediaPlayer对象时,它处于Idle(空闲)状态;调用setDataSource()方法后,MediaPlayer进入Initialized(已初始化)状态。
接着,调用prepare()或prepareAsync()方法使MediaPlayer进入Prepared(已准备)状态,此时可以调用start()方法开始播放。
在播放过程中,可以通过pause()方法暂停播放,进入Paused(已暂停)状态,再调用start()方法又会回到播放状态。
当播放完成后,MediaPlayer进入PlaybackCompleted(播放完成)状态,此时可以通过seekTo()方法重新定位到某个位置并再次播放。
最后,当不再需要MediaPlayer时,必须调用release()方法释放相关资源,否则会造成内存泄漏。
了解了基本概念后,让我们实际动手构建一个完整的视频播放器。
首先,我们需要设计一个简单的界面,包含一个SurfaceView和几个控制按钮:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="400dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btn_Play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="播放" />
<Button
android:id="@+id/btn_Pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="暂停" />
<Button
android:id="@+id/btn_Stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="停止" />
</LinearLayout>
</LinearLayout>
接下来,在Activity中初始化MediaPlayer和SurfaceView:
public class VideoActivity extends Activity {
private SurfaceView surfaceView;
private MediaPlayer player;
private SurfaceHolder holder;
private Button btnPlay, btnPause, btnStop;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video);
// 初始化UI组件
surfaceView = (SurfaceView) findViewById(R.id.surfaceView);
btnPlay = (Button) findViewById(R.id.btn_Play);
btnPause = (Button) findViewById(R.id.btn_Pause);
btnStop = (Button) findViewById(R.id.btn_Stop);
// 获取SurfaceHolder并添加回调
holder = surfaceView.getHolder();
holder.addCallback(new MyCallBack());
// 初始化MediaPlayer
player = new MediaPlayer();
// 设置按钮点击监听器
btnPlay.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
playVideo();
}
});
btnPause.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (player.isPlaying()) {
player.pause();
btnPause.setText("继续");
} else {
player.start();
btnPause.setText("暂停");
}
}
});
btnStop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (player.isPlaying()) {
player.stop();
player.reset();
}
}
});
}
private void playVideo() {
try {
// 重置MediaPlayer,防止多次播放出现问题
player.reset();
// 设置视频源,这里以网络视频为例
String videoUrl = "http://example.com/sample.mp4";
player.setDataSource(videoUrl);
// 设置音频流类型
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
// 设置显示在SurfaceView上
player.setDisplay(holder);
// 异步准备,避免阻塞UI线程
player.prepareAsync();
// 设置准备完成监听器
player.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
// 开始播放
player.start();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
// SurfaceHolder回调类
private class MyCallBack implements SurfaceHolder.Callback {
@Override
public void surfaceCreated(SurfaceHolder holder) {
// Surface创建完成,可以设置给MediaPlayer
if (player != null) {
player.setDisplay(holder);
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// Surface发生变化
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// Surface被销毁,需要释放MediaPlayer资源
if (player != null && player.isPlaying()) {
player.stop();
player.release();
player = null;
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (player != null) {
player.release();
player = null;
}
}
}
在实际应用中,我们还需要处理各种情况,比如屏幕方向改变、播放完成和播放错误等:
// 在playVideo方法中添加这些监听器
player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
// 播放完成
btnPause.setText("暂停");
Toast.makeText(VideoActivity.this, "播放完成", Toast.LENGTH_SHORT).show();
}
});
player.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// 处理错误
Toast.makeText(VideoActivity.this, "播放出错", Toast.LENGTH_SHORT).show();
return false;
}
});
// 在ConfigurationChanged时处理SurfaceView
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// 根据屏幕方向调整SurfaceView大小
ViewGroup.LayoutParams params = surfaceView.getLayoutParams();
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
// 横屏时全屏
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
params.height = ViewGroup.LayoutParams.MATCH_PARENT;
} else {
// 竖屏时固定高度
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
params.height = 400;
}
surfaceView.setLayoutParams(params);
}
SurfaceView之所以适合播放视频,是因为它实现了双缓冲机制。
什么是双缓冲?简单来说,就是在内存中创建两个绘图区域,一个用于前台显示,一个用于后台绘制。当后台绘制完成后,再交换到前台显示。
这种机制有效地解决了视频播放时的闪烁问题,保证了视频画面的流畅性。
与普通的View不同,SurfaceView的绘制可以在非UI线程中进行,这使得它特别适合用于视频播放和游戏开发等需要高性能绘制的场景。
SurfaceView通过SurfaceHolder来控制视频流。在使用时,我们需要先获取SurfaceHolder,然后通过它来管理SurfaceView的生命周期。
在实际开发中,你可能会遇到各种各样的问题。下面是一些常见的问题及解决方案:
当视频尺寸与SurfaceView尺寸不匹配时,视频可能会出现拉伸变形。可以通过以下方式保持视频原始比例:
// 在onPrepared方法中添加
player.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
// 根据视频尺寸调整SurfaceView大小,保持宽高比
adjustSurfaceViewSize(width, height);
}
});
private void adjustSurfaceViewSize(float videoWidth, float videoHeight) {
// 获取SurfaceView的宽度
int surfaceWidth = surfaceView.getWidth();
int surfaceHeight = surfaceView.getHeight();
// 计算视频宽高比
float videoRatio = videoWidth / videoHeight;
float surfaceRatio = (float) surfaceWidth / surfaceHeight;
ViewGroup.LayoutParams lp = surfaceView.getLayoutParams();
if (videoRatio > surfaceRatio) {
lp.width = surfaceWidth;
lp.height = (int) (surfaceWidth / videoRatio);
} else {
lp.width = (int) (surfaceHeight * videoRatio);
lp.height = surfaceHeight;
}
surfaceView.setLayoutParams(lp);
}
忘记释放MediaPlayer资源是常见的内存泄漏原因。务必在Activity销毁时正确释放:
@Override
protected void onPause() {
super.onPause();
if (player != null && player.isPlaying()) {
player.pause();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (player != null) {
player.stop();
player.release();
player = null;
}
}
对于网络视频,可以使用prepareAsync()方法异步准备,避免阻塞UI线程:
player.prepareAsync();
player.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
// 准备完成,开始播放
player.start();
}
});
// 添加缓冲监听
player.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
@Override
public void onBufferingUpdate(MediaPlayer mp, int percent) {
// 更新缓冲进度
updateBufferingProgress(percent);
}
});
下面是一个完整的视频播放器示例,包含进度控制、播放控制和全屏功能:
public class FullVideoPlayerActivity extends Activity
implements SurfaceHolder.Callback, SeekBar.OnSeekBarChangeListener {
private MediaPlayer player;
private SurfaceView surfaceView;
private SurfaceHolder holder;
private SeekBar progressBar;
private Button btnPlay, btnPause, btnStop, btnFullscreen;
private TextView currentTime, totalTime;
private Handler handler = new Handler();
private boolean isFullscreen = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_full_video_player);
initViews();
setupPlayer();
}
private void initViews() {
surfaceView = findViewById(R.id.surfaceView);
progressBar = findViewById(R.id.progressBar);
btnPlay = findViewById(R.id.btnPlay);
btnPause = findViewById(R.id.btnPause);
btnStop = findViewById(R.id.btnStop);
btnFullscreen = findViewById(R.id.btnFullscreen);
currentTime = findViewById(R.id.currentTime);
totalTime = findViewById(R.id.totalTime);
holder = surfaceView.getHolder();
holder.addCallback(this);
progressBar.setOnSeekBarChangeListener(this);
btnPlay.setOnClickListener(v -> playVideo());
btnPause.setOnClickListener(v -> pauseVideo());
btnStop.setOnClickListener(v -> stopVideo());
btnFullscreen.setOnClickListener(v -> toggleFullscreen());
}
private void setupPlayer() {
player = new MediaPlayer();
try {
player.setDataSource("http://example.com/sample.mp4");
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
player.setOnPreparedListener(mp -> {
progressBar.setMax(player.getDuration());
totalTime.setText(formatTime(player.getDuration()));
player.start();
updateProgress();
});
player.setOnCompletionListener(mp -> {
btnPlay.setEnabled(true);
btnPause.setEnabled(false);
});
} catch (IOException e) {
e.printStackTrace();
}
}
private void playVideo() {
if (!player.isPlaying()) {
player.start();
updateProgress();
}
}
private void pauseVideo() {
if (player.isPlaying()) {
player.pause();
}
}
private void stopVideo() {
if (player.isPlaying()) {
player.stop();
player.reset();
setupPlayer();
}
}
private void updateProgress() {
if (player.isPlaying()) {
progressBar.setProgress(player.getCurrentPosition());
currentTime.setText(formatTime(player.getCurrentPosition()));
handler.postDelayed(this::updateProgress, 1000);
}
}
private String formatTime(int milliseconds) {
int seconds = milliseconds / 1000;
int minutes = seconds / 60;
seconds = seconds % 60;
return String.format("%02d:%02d", minutes, seconds);
}
private void toggleFullscreen() {
if (isFullscreen) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
isFullscreen = !isFullscreen;
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
player.setDisplay(holder);
player.prepareAsync();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (player != null && player.isPlaying()) {
player.stop();
}
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser && player != null) {
player.seekTo(progress);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
@Override
protected void onDestroy() {
super.onDestroy();
if (player != null) {
player.release();
player = null;
}
handler.removeCallbacksAndMessages(null);
}
}
这个完整的示例包含了视频播放器的所有基本功能:播放控制、进度显示和调整、全屏切换等。
你可以根据自己的需求进一步扩展,比如添加播放列表、手势控制亮度/音量、播放速度调整等高级功能。
掌握MediaPlayer和SurfaceView的使用,就像是拿到了开启Android多媒体世界的钥匙。无论是简单的音频播放还是复杂的视频处理,都离不开这两个核心组件。现在,是时候动手实践,打造属于你自己的视频播放器了!