前几篇文章分析了mp4文件的格式和文件的解析,以及视频边缓冲边播放的原理讲解与代码实现,具体可以参看Android视频播放专题系列文章的讲解,本文就展示一下缓冲跳转代码的实现原理。
先分享一下4幅图片,分别为播放前的缓存,正常播放中,跳转缓冲和跳转以后的正常播放。
代码解析
视频断点分隔的数据结构定义
定义了每一段视频的时间偏移点,文件位移偏移点,文件段的大小和当前的缓存状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class VideoInfo { double timestart; long offsetstart; long offsetend; long downloadsize; DownloadStatus status; public VideoInfo() { status = DownloadStatus.NOTSTART; } @Override public String toString() { String s = "beginTime: |
class VideoInfo { double timestart; long offsetstart; long offsetend; long downloadsize; DownloadStatus status; public VideoInfo() { status = DownloadStatus.NOTSTART; } @Override public String toString() { String s = "beginTime:
缓存状态的定义
分别为还未缓存,正在下载中和缓存完毕
1 2 3 | enum DownloadStatus { NOTSTART, DOWNLOADING, FINISH, } |
enum DownloadStatus { NOTSTART, DOWNLOADING, FINISH, }
分段视频的下载
利用了Http协议的 Range 属性,下载部分文件
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 | private void downloadbyvideoinfo(VideoInfo vi) throws IOException { System.out.println("download -> " + vi.toString()); URL url = new URL(this.url); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(3000); conn.setRequestProperty("Range", "bytes=" + vi.offsetstart + "-" + vi.offsetend); RandomAccessFile raf = new RandomAccessFile(new File(localFilePath), "rws"); raf.seek(vi.offsetstart); InputStream in = conn.getInputStream(); byte[] buf = new byte[1024 * 10]; int len; vi.downloadsize = 0; while ((len = in.read(buf)) != -1) { raf.write(buf, 0, len); vi.downloadsize += len; Message msg = handler.obtainMessage(); msg.what = MSG_DOWNLOADUPDATE; msg.obj = vi.offsetstart + vi.downloadsize; handler.sendMessage(msg); } in.close(); raf.close(); } |
private void downloadbyvideoinfo(VideoInfo vi) throws IOException { System.out.println("download -> " + vi.toString()); URL url = new URL(this.url); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(3000); conn.setRequestProperty("Range", "bytes=" + vi.offsetstart + "-" + vi.offsetend); RandomAccessFile raf = new RandomAccessFile(new File(localFilePath), "rws"); raf.seek(vi.offsetstart); InputStream in = conn.getInputStream(); byte[] buf = new byte[1024 * 10]; int len; vi.downloadsize = 0; while ((len = in.read(buf)) != -1) { raf.write(buf, 0, len); vi.downloadsize += len; Message msg = handler.obtainMessage(); msg.what = MSG_DOWNLOADUPDATE; msg.obj = vi.offsetstart + vi.downloadsize; handler.sendMessage(msg); } in.close(); raf.close(); }
检测指定时间点的视频数据是否已经缓存完毕
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 | public boolean checkIsBuffered(long time) { int index = -1; for (VideoDownloder.VideoInfo tvi : vilists) { if (tvi.timestart > time) { break; } index++; } if (index < 0 || index >= this.vilists.size()) { return true; } final VideoInfo vi = this.vilists.get(index); if (vi.status == DownloadStatus.FINISH) { return true; } else if (vi.status == DownloadStatus.NOTSTART) { return false; } else if (vi.status == DownloadStatus.DOWNLOADING) { return (vi.downloadsize * 100 / (vi.offsetend - vi.offsetstart)) > ((time - vi.timestart) * 100 / SEP_SECOND); } return true; } |
public boolean checkIsBuffered(long time) { int index = -1; for (VideoDownloder.VideoInfo tvi : vilists) { if (tvi.timestart > time) { break; } index++; } if (index < 0 || index >= this.vilists.size()) { return true; } final VideoInfo vi = this.vilists.get(index); if (vi.status == DownloadStatus.FINISH) { return true; } else if (vi.status == DownloadStatus.NOTSTART) { return false; } else if (vi.status == DownloadStatus.DOWNLOADING) { return (vi.downloadsize * 100 / (vi.offsetend - vi.offsetstart)) > ((time - vi.timestart) * 100 / SEP_SECOND); } return true; }
跳转后加载指定时间点的视频数据
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 | public synchronized void seekLoadVideo(long time) { int index = -1; for (VideoDownloder.VideoInfo tvi : vilists) { if (tvi.timestart > time) { break; } index++; } if (index < 0 || index >= this.vilists.size()) { return; } final VideoInfo vi = this.vilists.get(index); if (vi.status == DownloadStatus.NOTSTART) { executorService.submit(new Runnable() { @Override public void run() { try { vi.status = DownloadStatus.DOWNLOADING; downloadbyvideoinfo(vi); vi.status = DownloadStatus.FINISH; } catch (IOException e) { vi.status = DownloadStatus.NOTSTART; e.printStackTrace(); } } }); } downloadvideoindex = index; } |
public synchronized void seekLoadVideo(long time) { int index = -1; for (VideoDownloder.VideoInfo tvi : vilists) { if (tvi.timestart > time) { break; } index++; } if (index < 0 || index >= this.vilists.size()) { return; } final VideoInfo vi = this.vilists.get(index); if (vi.status == DownloadStatus.NOTSTART) { executorService.submit(new Runnable() { @Override public void run() { try { vi.status = DownloadStatus.DOWNLOADING; downloadbyvideoinfo(vi); vi.status = DownloadStatus.FINISH; } catch (IOException e) { vi.status = DownloadStatus.NOTSTART; e.printStackTrace(); } } }); } downloadvideoindex = index; }
视频分割数据结构的初始化
目前以5s进行视频的分段处理
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 | private static final int SEP_SECOND = 5; public void initVideoDownloder(final long startoffset, final long totalsize) { if (isinitok) { return; } this.executorService.submit(new Runnable() { @Override public void run() { IsoFile isoFile = null; try { isoFile = new IsoFile(new RandomAccessFile(localFilePath, "r").getChannel()); } catch (Exception e) { e.printStackTrace(); } if (isoFile == null) { return; } CareyMp4Parser cmp4p = new CareyMp4Parser(isoFile); cmp4p.printinfo(); vilists.clear(); VideoInfo vi = null; for (int i = 0; i < cmp4p.syncSamples.length; i++) { if (vi == null) { vi = new VideoInfo(); vi.timestart = cmp4p.timeOfSyncSamples[i]; vi.offsetstart = cmp4p.syncSamplesOffset[i]; } if (cmp4p.timeOfSyncSamples[i] < (vilists.size() + 1) * SEP_SECOND) { continue; } vi.offsetend = cmp4p.syncSamplesOffset[i]; vilists.add(vi); vi = null; i--; } if (vi != null) { vi.offsetend = totalsize; vilists.add(vi); vi = null; } isinitok = true; downloadvideo(startoffset); } }); } |
private static final int SEP_SECOND = 5; public void initVideoDownloder(final long startoffset, final long totalsize) { if (isinitok) { return; } this.executorService.submit(new Runnable() { @Override public void run() { IsoFile isoFile = null; try { isoFile = new IsoFile(new RandomAccessFile(localFilePath, "r").getChannel()); } catch (Exception e) { e.printStackTrace(); } if (isoFile == null) { return; } CareyMp4Parser cmp4p = new CareyMp4Parser(isoFile); cmp4p.printinfo(); vilists.clear(); VideoInfo vi = null; for (int i = 0; i < cmp4p.syncSamples.length; i++) { if (vi == null) { vi = new VideoInfo(); vi.timestart = cmp4p.timeOfSyncSamples[i]; vi.offsetstart = cmp4p.syncSamplesOffset[i]; } if (cmp4p.timeOfSyncSamples[i] < (vilists.size() + 1) * SEP_SECOND) { continue; } vi.offsetend = cmp4p.syncSamplesOffset[i]; vilists.add(vi); vi = null; i--; } if (vi != null) { vi.offsetend = totalsize; vilists.add(vi); vi = null; } isinitok = true; downloadvideo(startoffset); } }); }
MP4视频头部数据的缓存
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 | private void prepareVideo() throws IOException { URL url = new URL(remoteUrl); HttpURLConnection httpConnection = (HttpURLConnection) url .openConnection(); httpConnection.setConnectTimeout(3000); httpConnection.setRequestProperty("RANGE", "bytes=" + 0 + "-"); InputStream is = httpConnection.getInputStream(); videoTotalSize = httpConnection.getContentLength(); if (videoTotalSize == -1) { return; } File cacheFile = new File(localUrl); if (!cacheFile.exists()) { cacheFile.getParentFile().mkdirs(); cacheFile.createNewFile(); } RandomAccessFile raf = new RandomAccessFile(cacheFile, "rws"); raf.setLength(videoTotalSize); raf.seek(0); byte buf[] = new byte[10 * 1024]; int size = 0; videoCacheSize = 0; int buffercnt = 0; while ((size = is.read(buf)) != -1 && (!isready)) { try { raf.write(buf, 0, size); videoCacheSize += size; } catch (Exception e) { e.printStackTrace(); } if (videoCacheSize > READY_BUFF && (buffercnt++ % 20 == 0)) { mHandler.sendEmptyMessage(CACHE_VIDEO_READY); } } if (!isready) { mHandler.sendEmptyMessage(CACHE_VIDEO_READY); } is.close(); raf.close(); } |
private void prepareVideo() throws IOException { URL url = new URL(remoteUrl); HttpURLConnection httpConnection = (HttpURLConnection) url .openConnection(); httpConnection.setConnectTimeout(3000); httpConnection.setRequestProperty("RANGE", "bytes=" + 0 + "-"); InputStream is = httpConnection.getInputStream(); videoTotalSize = httpConnection.getContentLength(); if (videoTotalSize == -1) { return; } File cacheFile = new File(localUrl); if (!cacheFile.exists()) { cacheFile.getParentFile().mkdirs(); cacheFile.createNewFile(); } RandomAccessFile raf = new RandomAccessFile(cacheFile, "rws"); raf.setLength(videoTotalSize); raf.seek(0); byte buf[] = new byte[10 * 1024]; int size = 0; videoCacheSize = 0; int buffercnt = 0; while ((size = is.read(buf)) != -1 && (!isready)) { try { raf.write(buf, 0, size); videoCacheSize += size; } catch (Exception e) { e.printStackTrace(); } if (videoCacheSize > READY_BUFF && (buffercnt++ % 20 == 0)) { mHandler.sendEmptyMessage(CACHE_VIDEO_READY); } } if (!isready) { mHandler.sendEmptyMessage(CACHE_VIDEO_READY); } is.close(); raf.close(); }
主程序的状态控制
主要就是首先缓冲mp4的文件头,然后进行解析和播放,初始化播放器控制界面,同时为了提高用户体验,动态监测当前播放的进度和当前缓存的进度,目前的做法是至少缓存了超前5s的数据流,即如果当前播放在00:02:05,则理论上缓存的数据已经在00:02:10之后了,如果没有达到这个状态,则暂停当前的视频播放,先进行一段缓存,然后再接着播放,这样就不会出现视频播放的过程中出现一卡一卡的情况。
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 | private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case VIDEO_STATE_UPDATE: int cachepercent = (int) (videoCacheSize * 100 / (videoTotalSize == 0 ? 1 : videoTotalSize)); videoseekbar.setSecondaryProgress(cachepercent); int positon = mediaPlayer.getCurrentPosition(); int duration = mediaPlayer.getDuration(); int playpercent = positon * 100 / (duration == 0 ? 1 : duration); if (mediaPlayer.isPlaying()) { playbtn.setImageResource(R.drawable.player_pad_button_pause_normal); tvcurtime.setText(transtimetostr(positon)); tvtotaltime.setText(transtimetostr(duration)); videoseekbar.setProgress(playpercent); int next2sec = positon + 2 * 1000; if (next2sec > duration) { next2sec = duration; } if (!vdl.checkIsBuffered(next2sec / 1000)) { mediaPlayer.pause(); showLoading(); } } else { playbtn.setImageResource(R.drawable.player_pad_button_play_normal); if (!userpaused && isloading) { int next5sec = positon + 5 * 1000; if (next5sec > duration) { next5sec = duration; } if (vdl.checkIsBuffered(next5sec / 1000)) { mediaPlayer.start(); hideLoading(); } } } mHandler.sendEmptyMessageDelayed(VIDEO_STATE_UPDATE, 1000); break; case CACHE_VIDEO_READY: try { mediaPlayer.setDataSource(localUrl); mediaPlayer.prepareAsync(); } catch (Exception e) { e.printStackTrace(); } break; case VideoDownloder.MSG_DOWNLOADFINISH: videoCacheSize = videoTotalSize; break; case VideoDownloder.MSG_DOWNLOADUPDATE: if ((Long) msg.obj > videoCacheSize) { videoCacheSize = (Long) msg.obj; } break; } super.handleMessage(msg); } }; |
private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case VIDEO_STATE_UPDATE: int cachepercent = (int) (videoCacheSize * 100 / (videoTotalSize == 0 ? 1 : videoTotalSize)); videoseekbar.setSecondaryProgress(cachepercent); int positon = mediaPlayer.getCurrentPosition(); int duration = mediaPlayer.getDuration(); int playpercent = positon * 100 / (duration == 0 ? 1 : duration); if (mediaPlayer.isPlaying()) { playbtn.setImageResource(R.drawable.player_pad_button_pause_normal); tvcurtime.setText(transtimetostr(positon)); tvtotaltime.setText(transtimetostr(duration)); videoseekbar.setProgress(playpercent); int next2sec = positon + 2 * 1000; if (next2sec > duration) { next2sec = duration; } if (!vdl.checkIsBuffered(next2sec / 1000)) { mediaPlayer.pause(); showLoading(); } } else { playbtn.setImageResource(R.drawable.player_pad_button_play_normal); if (!userpaused && isloading) { int next5sec = positon + 5 * 1000; if (next5sec > duration) { next5sec = duration; } if (vdl.checkIsBuffered(next5sec / 1000)) { mediaPlayer.start(); hideLoading(); } } } mHandler.sendEmptyMessageDelayed(VIDEO_STATE_UPDATE, 1000); break; case CACHE_VIDEO_READY: try { mediaPlayer.setDataSource(localUrl); mediaPlayer.prepareAsync(); } catch (Exception e) { e.printStackTrace(); } break; case VideoDownloder.MSG_DOWNLOADFINISH: videoCacheSize = videoTotalSize; break; case VideoDownloder.MSG_DOWNLOADUPDATE: if ((Long) msg.obj > videoCacheSize) { videoCacheSize = (Long) msg.obj; } break; } super.handleMessage(msg); } };
后续
大体的代码流程就是这个样子,鉴于代码量比较大,就不全部贴出来了,相信大家整体浏览一遍也清楚了,源代码文件可以在点击这里下载,如果还有相关的问题,请给我留言。
博文 最后的链接貌似 失效了,能给我发一份吗?多谢楼主 376148219@qq.com
有源码吗
@ゞ星梦缘︷:博文的最后有下载链接地址,可以获取全部源码
我觉得还是很有问题的,对于几百M的视频文件来说 raf.setLength(videoTotalSize);和raf.seek(vi.offsetstart); 方法要执行很长的时间,是不是啊?
@一叶随风: 大文件没有测试过,可能有效率的影响
源码里CareyMp4Parser是什么呢?
这是我自己写的解析MP4格式的类,可以在代码中找到,主要是用来解析mp4的文件头来计算内部视音频数据的文件偏移量,用来做拖动处理计算。
请问IosFile这个是需要导入外部包吗?