1. 程式人生 > >Android視訊播放之邊快取邊播放

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

大家可以在實際環境中做測試,和依據實際情況動態維護快取區,在具體的使用過程中有神馬問題留言即可!