最近在做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);
    }
};

总体来说,原理比较简单,就是把视频加载了一段后,就送到播放器播放,如果出现了错误,则优先缓存一部分文件,然后再继续播放,类似的处理过程循环往复。