现在的位置: 首页 > 移动开发> 正文
Android 流媒体播放之缓冲跳转实现
2012年05月29日 移动开发 评论数 8 ⁄ 被围观 10,095+

前几篇文章分析了mp4文件的格式和文件的解析,以及视频边缓冲边播放的原理讲解与代码实现,具体可以参看Android视频播放专题系列文章的讲解,本文就展示一下缓冲跳转代码的实现原理。

先分享一下4幅图片,分别为播放前的缓存,正常播放中,跳转缓冲和跳转以后的正常播放。
huanchongqian
bofangzhong
tiaozhuanghuanchong
tiaozhuangbofang

代码解析

视频断点分隔的数据结构定义

定义了每一段视频的时间偏移点,文件位移偏移点,文件段的大小和当前的缓存状态

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);
        }
    };

后续

大体的代码流程就是这个样子,鉴于代码量比较大,就不全部贴出来了,相信大家整体浏览一遍也清楚了,源代码文件可以在点击这里下载,如果还有相关的问题,请给我留言。

目前有 8 条留言 其中:访客:7 条, 博主:1 条

  1. 李俊 : 2014年09月23日16:21:13  -49楼 @回复 回复

    博文 最后的链接貌似 失效了,能给我发一份吗?多谢楼主 376148219@qq.com

  2. ゞ星梦缘︷ : 2013年05月20日12:30:23  -48楼 @回复 回复

    有源码吗

    • 周润生 : 2013年05月21日20:24:43 @回复 回复

      @ゞ星梦缘︷:博文的最后有下载链接地址,可以获取全部源码

  3. 一叶随风 : 2012年08月28日14:41:38  -47楼 @回复 回复

    我觉得还是很有问题的,对于几百M的视频文件来说 raf.setLength(videoTotalSize);和raf.seek(vi.offsetstart); 方法要执行很长的时间,是不是啊?

    • 周润生 : 2013年05月21日20:26:07 @回复 回复

      @一叶随风: 大文件没有测试过,可能有效率的影响

  4. accordall : 2012年08月23日17:03:36  -46楼 @回复 回复

    源码里CareyMp4Parser是什么呢?

    • 润物无声 : 2012年08月28日09:22:01 @回复 回复

      这是我自己写的解析MP4格式的类,可以在代码中找到,主要是用来解析mp4的文件头来计算内部视音频数据的文件偏移量,用来做拖动处理计算。

  5. zin.yan : 2012年06月25日13:29:16  -45楼 @回复 回复

    请问IosFile这个是需要导入外部包吗?

给我留言

留言无头像?


×
腾讯微博