最近在做Android视频播放的有关项目,其中有一项需求就是要求视频可以边加载缓存边播放,类似于优酷土豆的视频点播。网上找了一些相关的资料,比较了每种视频格式的优缺点之后,结合Android手机自身的优势,默认支持mp4编码和解码,最终采用mp4格式作为视频的存储格式。
其实最真实的流媒体协议传输格式并不是普通的http方式,而是rtsp,那样的话得搭建专门的流媒体服务器,成本比较高,采用普通的http方式,实现的是一种伪流媒体传输,但是对于常用的视频缓存播放也足够了。
本文的目的就是给大家介绍一种以此原理而开发一个Android视频边缓存边播放的示例,通过该示例的学习,相信大家能对该原理有更深入的理解。
代码解析
VideoViewDemo.java 主要是用来设置启动参数,设定网络视频的url地址和本地缓存的地址,本地缓存的地址可以不设置,程序会自己维护,如果您自己设置了,视频就会缓存到该位置。
public class VideoViewDemo extends Activity {
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
//String url = "http://carey-blog-image.googlecode.com/files/vid_20120510_090204.mp4";
String url = "http://bbfile.b0.upaiyun.com/data/videos/2/vid_20120510_090204.mp4";
Intent intent = new Intent();
intent.setClass(VideoViewDemo.this, BBVideoPlayer.class);
intent.putExtra("url", url);
intent.putExtra("cache",
Environment.getExternalStorageDirectory().getAbsolutePath()
+ "/VideoCache/" + System.currentTimeMillis() + ".mp4");
startActivity(intent);
}
}
BBVideoPlayer.java 就是视频缓存的核心了,READYBUFF定义了初始缓存区的大小,当视频加载到初始缓存区满的时候,播放器开始播放,CACHEBUFF则是核心交换缓存区,主要是用来动态调节缓存区,当网络环境较好的时候,该缓存区为初始大小,当网络环境差的时候,该缓存区会动态增加,主要就是为了避免视频播放的时候出现一卡一卡的现象。
public class BBVideoPlayer extends Activity {
private VideoView mVideoView;
private TextView tvcache;
private String remoteUrl;
private String localUrl;
private ProgressDialog progressDialog = null;
private static final int READY_BUFF = 2000 * 1024;
private static final int CACHE_BUFF = 500 * 1024;
private boolean isready = false;
private boolean iserror = false;
private int errorCnt = 0;
private int curPosition = 0;
private long mediaLength = 0;
private long readSize = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.bbvideoplayer);
findViews();
init();
playvideo();
}
private void findViews() {
this.mVideoView = (VideoView) findViewById(R.id.bbvideoview);
this.tvcache = (TextView) findViewById(R.id.tvcache);
}
private void init() {
Intent intent = getIntent();
this.remoteUrl = intent.getStringExtra("url");
System.out.println("remoteUrl: " + remoteUrl);
if (this.remoteUrl == null) {
finish();
return;
}
this.localUrl = intent.getStringExtra("cache");
mVideoView.setMediaController(new MediaController(this));
mVideoView.setOnPreparedListener(new OnPreparedListener() {
public void onPrepared(MediaPlayer mediaplayer) {
dismissProgressDialog();
mVideoView.seekTo(curPosition);
mediaplayer.start();
}
});
mVideoView.setOnCompletionListener(new OnCompletionListener() {
public void onCompletion(MediaPlayer mediaplayer) {
curPosition = 0;
mVideoView.pause();
}
});
mVideoView.setOnErrorListener(new OnErrorListener() {
public boolean onError(MediaPlayer mediaplayer, int i, int j) {
iserror = true;
errorCnt++;
mVideoView.pause();
showProgressDialog();
return true;
}
});
}
private void showProgressDialog() {
mHandler.post(new Runnable() {
@Override
public void run() {
if (progressDialog == null) {
progressDialog = ProgressDialog.show(BBVideoPlayer.this,
"视频缓存", "正在努力加载中 ...", true, false);
}
}
});
}
private void dismissProgressDialog() {
mHandler.post(new Runnable() {
@Override
public void run() {
if (progressDialog != null) {
progressDialog.dismiss();
progressDialog = null;
}
}
});
}
private void playvideo() {
if (!URLUtil.isNetworkUrl(this.remoteUrl)) {
mVideoView.setVideoPath(this.remoteUrl);
mVideoView.start();
return;
}
showProgressDialog();
new Thread(new Runnable() {
@Override
public void run() {
FileOutputStream out = null;
InputStream is = null;
try {
URL url = new URL(remoteUrl);
HttpURLConnection httpConnection = (HttpURLConnection) url
.openConnection();
if (localUrl == null) {
localUrl = Environment.getExternalStorageDirectory()
.getAbsolutePath()
+ "/VideoCache/"
+ System.currentTimeMillis() + ".mp4";
}
System.out.println("localUrl: " + localUrl);
File cacheFile = new File(localUrl);
if (!cacheFile.exists()) {
cacheFile.getParentFile().mkdirs();
cacheFile.createNewFile();
}
readSize = cacheFile.length();
out = new FileOutputStream(cacheFile, true);
httpConnection.setRequestProperty("User-Agent", "NetFox");
httpConnection.setRequestProperty("RANGE", "bytes="
+ readSize + "-");
is = httpConnection.getInputStream();
mediaLength = httpConnection.getContentLength();
if (mediaLength == -1) {
return;
}
mediaLength += readSize;
byte buf[] = new byte[4 * 1024];
int size = 0;
long lastReadSize = 0;
mHandler.sendEmptyMessage(VIDEO_STATE_UPDATE);
while ((size = is.read(buf)) != -1) {
try {
out.write(buf, 0, size);
readSize += size;
} catch (Exception e) {
e.printStackTrace();
}
if (!isready) {
if ((readSize - lastReadSize) > READY_BUFF) {
lastReadSize = readSize;
mHandler.sendEmptyMessage(CACHE_VIDEO_READY);
}
} else {
if ((readSize - lastReadSize) > CACHE_BUFF
* (errorCnt + 1)) {
lastReadSize = readSize;
mHandler.sendEmptyMessage(CACHE_VIDEO_UPDATE);
}
}
}
mHandler.sendEmptyMessage(CACHE_VIDEO_END);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
//
}
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
//
}
}
}
}
}).start();
}
private final static int VIDEO_STATE_UPDATE = 0;
private final static int CACHE_VIDEO_READY = 1;
private final static int CACHE_VIDEO_UPDATE = 2;
private final static int CACHE_VIDEO_END = 3;
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case VIDEO_STATE_UPDATE:
double cachepercent = readSize * 100.00 / mediaLength * 1.0;
String s = String.format("已缓存: [%.2f%%]", cachepercent);
if (mVideoView.isPlaying()) {
curPosition = mVideoView.getCurrentPosition();
int duration = mVideoView.getDuration();
duration = duration == 0 ? 1 : duration;
double playpercent = curPosition * 100.00 / duration * 1.0;
int i = curPosition / 1000;
int hour = i / (60 * 60);
int minute = i / 60 % 60;
int second = i % 60;
s += String.format(" 播放: %02d:%02d:%02d [%.2f%%]", hour,
minute, second, playpercent);
}
tvcache.setText(s);
mHandler.sendEmptyMessageDelayed(VIDEO_STATE_UPDATE, 1000);
break;
case CACHE_VIDEO_READY:
isready = true;
mVideoView.setVideoPath(localUrl);
mVideoView.start();
break;
case CACHE_VIDEO_UPDATE:
if (iserror) {
mVideoView.setVideoPath(localUrl);
mVideoView.start();
iserror = false;
}
break;
case CACHE_VIDEO_END:
if (iserror) {
mVideoView.setVideoPath(localUrl);
mVideoView.start();
iserror = false;
}
break;
}
super.handleMessage(msg);
}
};
总体来说,原理比较简单,就是把视频加载了一段后,就送到播放器播放,如果出现了错误,则优先缓存一部分文件,然后再继续播放,类似的处理过程循环往复。