Android視訊播放之邊快取邊播放
轉載地址:http://blog.zhourunsheng.com/2012/05/android%e8%a7%86%e9%a2%91%e6%92%ad%e6%94%be%e4%b9%8b%e8%be%b9%e7%bc%93%e5%ad%98%e8%be%b9%e6%92%ad%e6%94%be/
最近在做Android視訊播放的有關專案,其中有一項需求就是要求視訊可以邊載入快取邊播放,類似於優酷土豆的視訊點播。網上找了一些相關的資料,比較了每種視訊格式的優缺點之後,結合Android手機自身的優勢,預設支援mp4編碼和解碼,最終採用mp4格式作為視訊的儲存格式。
其實最真實的流媒體協議傳輸格式並不是普通的http方式,而是rtsp,那樣的話得搭建專門的流媒體伺服器,成本比較高,採用普通的http方式,實現的是一種偽流媒體傳輸,但是對於常用的視訊快取播放也足夠了。
要想實現視訊的邊快取邊播放,原則上就要求視訊的儲存格式是分段的,而mp4正好滿足這個要求,只要將mp4的整體視訊資訊放在mp4檔案的開頭,這樣只要載入了mp4檔案的頭部之後,就能解析出該mp4檔案的時長,位元率等等,為後續的視訊快取做初始化設定,然後每載入一段mp4檔案的資料流,通過解析頭部來或得當前視訊流的幀資訊,並在播放器中播放,這樣就能先載入一段進行播放,同時快取後續的一段,依此原理就能實現。
本文的目的就是給大家介紹一種以此原理而開發一個Android視訊邊快取邊播放的示例,通過該示例的學習,相信大家能對該原理有更深入的理解。
在介紹具體的demo之前,先放上幾張截圖,分別為視訊播放前的快取,視訊邊快取邊播放,快取完畢的視訊播放。
程式碼解析
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則是核心交換快取區,主要是用來動態調節快取區,當網路環境較好的時候,該快取區為初始大小,當網路環境差的時候,該快取區會動態增加,主要就是為了避免視訊播放的時候出現一卡一卡的現象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
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); } }; } 總體來說,原理比較簡單,就是把視訊載入了一段後,就送到播放器播放,如果出現了錯誤,則優先快取一部分檔案,然後再繼續播放,類似的處理過程迴圈往復。 程式原始碼下載: code 大家可以在實際環境中做測試,和依據實際情況動態維護快取區,在具體的使用過程中有神馬問題留言即可! |