<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>润物无声 &#187; 视频</title>
	<atom:link href="http://blog.zhourunsheng.com/tag/%e8%a7%86%e9%a2%91/feed/" rel="self" type="application/rss+xml" />
	<link>http://blog.zhourunsheng.com</link>
	<description>天空一朵雨做的云</description>
	<lastBuildDate>Sat, 08 May 2021 05:17:21 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=4.1.41</generator>
	<item>
		<title>Android 流媒体播放之缓冲跳转实现</title>
		<link>http://blog.zhourunsheng.com/2012/05/android-%e6%b5%81%e5%aa%92%e4%bd%93%e6%92%ad%e6%94%be%e4%b9%8b%e7%bc%93%e5%86%b2%e8%b7%b3%e8%bd%ac%e5%ae%9e%e7%8e%b0/</link>
		<comments>http://blog.zhourunsheng.com/2012/05/android-%e6%b5%81%e5%aa%92%e4%bd%93%e6%92%ad%e6%94%be%e4%b9%8b%e7%bc%93%e5%86%b2%e8%b7%b3%e8%bd%ac%e5%ae%9e%e7%8e%b0/#comments</comments>
		<pubDate>Tue, 29 May 2012 06:06:09 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[mp4]]></category>
		<category><![CDATA[流媒体]]></category>
		<category><![CDATA[视频]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1595</guid>
		<description><![CDATA[<p>前几篇文章分析了mp4文件的格式和文件的解析，以及视频边缓冲边播放的原理讲解与代码实现，具体可以参看Andro [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/05/android-%e6%b5%81%e5%aa%92%e4%bd%93%e6%92%ad%e6%94%be%e4%b9%8b%e7%bc%93%e5%86%b2%e8%b7%b3%e8%bd%ac%e5%ae%9e%e7%8e%b0/">Android 流媒体播放之缓冲跳转实现</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>前几篇文章分析了mp4文件的格式和文件的解析，以及视频边缓冲边播放的原理讲解与代码实现，具体可以参看<a href="http://blog.zhourunsheng.com/tag/android/">Android视频播放专题系列文章</a>的讲解，本文就展示一下缓冲跳转代码的实现原理。</p>
<p>先分享一下4幅图片，分别为播放前的缓存，正常播放中，跳转缓冲和跳转以后的正常播放。<br />
<img class="alignleft" src="http://blog.zhourunsheng.com/wp-content/uploads/2012/06/huanchongqian.png" alt="huanchongqian" width="160" height="240" /><br />
<img class="alignleft" src="http://blog.zhourunsheng.com/wp-content/uploads/2012/06/bofangzhong.png" alt="bofangzhong" width="160" height="240" /><br />
<img class="alignleft" src="http://blog.zhourunsheng.com/wp-content/uploads/2012/06/tiaozhuanghuanchong.png" alt="tiaozhuanghuanchong" width="160" height="240" /><br />
<img src="http://blog.zhourunsheng.com/wp-content/uploads/2012/06/tiaozhuangbofang.png" alt="tiaozhuangbofang" width="160" height="240" /><br />
<span id="more-1595"></span></p>
<h2>代码解析</h2>
<p><strong>视频断点分隔的数据结构定义</strong></p>
<p>定义了每一段视频的时间偏移点，文件位移偏移点，文件段的大小和当前的缓存状态</p>
<pre>class VideoInfo {
        double timestart;
        long offsetstart;
        long offsetend;
        long downloadsize;
        DownloadStatus status;

        public VideoInfo() {
            status = DownloadStatus.NOTSTART;
        }

        @Override
        public String toString() {
            String s = "beginTime:</pre>
<p><strong>缓存状态的定义</strong></p>
<p>分别为还未缓存，正在下载中和缓存完毕</p>
<pre>    enum DownloadStatus {
        NOTSTART, DOWNLOADING, FINISH,
    }</pre>
<p><strong>分段视频的下载</strong></p>
<p>利用了Http协议的 Range 属性，下载部分文件</p>
<pre>private void downloadbyvideoinfo(VideoInfo vi) throws IOException {
        System.out.println("download -&gt; " + 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();
    }</pre>
<p><strong>检测指定时间点的视频数据是否已经缓存完毕</strong></p>
<pre>    public boolean checkIsBuffered(long time) {
        int index = -1;

        for (VideoDownloder.VideoInfo tvi : vilists) {
            if (tvi.timestart &gt; time) {
                break;
            }

            index++;
        }

        if (index &lt; 0 || index &gt;= 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)) &gt; ((time - vi.timestart) * 100 / SEP_SECOND);
        }

        return true;
    }</pre>
<p><strong>跳转后加载指定时间点的视频数据</strong></p>
<pre>    public synchronized void seekLoadVideo(long time) {
        int index = -1;

        for (VideoDownloder.VideoInfo tvi : vilists) {
            if (tvi.timestart &gt; time) {
                break;
            }
            index++;
        }

        if (index &lt; 0 || index &gt;= 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;
    }</pre>
<p><strong>视频分割数据结构的初始化</strong></p>
<p>目前以5s进行视频的分段处理</p>
<pre>    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 &lt; cmp4p.syncSamples.length; i++) {
                    if (vi == null) {
                        vi = new VideoInfo();
                        vi.timestart = cmp4p.timeOfSyncSamples[i];
                        vi.offsetstart = cmp4p.syncSamplesOffset[i];
                    }

                    if (cmp4p.timeOfSyncSamples[i] &lt; (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);
            }
        });
    }</pre>
<p><strong>MP4视频头部数据的缓存</strong></p>
<pre>    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 &amp;&amp; (!isready)) {
            try {
                raf.write(buf, 0, size);
                videoCacheSize += size;
            } catch (Exception e) {
                e.printStackTrace();
            }

            if (videoCacheSize &gt; READY_BUFF &amp;&amp; (buffercnt++ % 20 == 0)) {
                mHandler.sendEmptyMessage(CACHE_VIDEO_READY);
            }
        }

        if (!isready) {
            mHandler.sendEmptyMessage(CACHE_VIDEO_READY);
        }

        is.close();
        raf.close();
    }</pre>
<p><strong>主程序的状态控制</strong></p>
<p>主要就是首先缓冲mp4的文件头，然后进行解析和播放，初始化播放器控制界面，同时为了提高用户体验，动态监测当前播放的进度和当前缓存的进度，目前的做法是至少缓存了超前5s的数据流，即如果当前播放在00:02:05，则理论上缓存的数据已经在00:02:10之后了，如果没有达到这个状态，则暂停当前的视频播放，先进行一段缓存，然后再接着播放，这样就不会出现视频播放的过程中出现一卡一卡的情况。</p>
<pre>    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 &gt; duration) {
                        next2sec = duration;
                    }

                    if (!vdl.checkIsBuffered(next2sec / 1000)) {
                        mediaPlayer.pause();
                        showLoading();
                    }
                } else {
                    playbtn.setImageResource(R.drawable.player_pad_button_play_normal);

                    if (!userpaused &amp;&amp; isloading) {
                        int next5sec = positon + 5 * 1000;
                        if (next5sec &gt; 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 &gt; videoCacheSize) {
                    videoCacheSize = (Long) msg.obj;
                }
                break;
            }

            super.handleMessage(msg);
        }
    };</pre>
<p><strong>后续</strong></p>
<p>大体的代码流程就是这个样子,鉴于代码量比较大，就不全部贴出来了，相信大家整体浏览一遍也清楚了，源代码文件可以在<a title="BBVideoPlayer.java" href="http://carey-blog-image.googlecode.com/files/BBVideoPlayer%2820120529%29.java">点击这里</a>下载，如果还有相关的问题，请给我留言。</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/05/android-%e6%b5%81%e5%aa%92%e4%bd%93%e6%92%ad%e6%94%be%e4%b9%8b%e7%bc%93%e5%86%b2%e8%b7%b3%e8%bd%ac%e5%ae%9e%e7%8e%b0/">Android 流媒体播放之缓冲跳转实现</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/05/android-%e6%b5%81%e5%aa%92%e4%bd%93%e6%92%ad%e6%94%be%e4%b9%8b%e7%bc%93%e5%86%b2%e8%b7%b3%e8%bd%ac%e5%ae%9e%e7%8e%b0/feed/</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>编译 faplayer 播放器</title>
		<link>http://blog.zhourunsheng.com/2012/05/%e7%bc%96%e8%af%91-faplayer-%e6%92%ad%e6%94%be%e5%99%a8/</link>
		<comments>http://blog.zhourunsheng.com/2012/05/%e7%bc%96%e8%af%91-faplayer-%e6%92%ad%e6%94%be%e5%99%a8/#comments</comments>
		<pubDate>Sat, 26 May 2012 05:17:31 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[faplayer]]></category>
		<category><![CDATA[Video]]></category>
		<category><![CDATA[视频]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1582</guid>
		<description><![CDATA[<p>最近在做一个有关视频播放的项目，涉及到的问题就是视频解码，目前最优秀的两套解码库就是ffmpeg和flv，几乎 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/05/%e7%bc%96%e8%af%91-faplayer-%e6%92%ad%e6%94%be%e5%99%a8/">编译 faplayer 播放器</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>最近在做一个有关视频播放的项目，涉及到的问题就是视频解码，目前最优秀的两套解码库就是ffmpeg和flv，几乎现在能见到的所有视频格式都能解码，本文编译的faplayer就是基于flv的解码库。</p>
<p>编译成功的程序可以正常播放优酷的flv格式，截图如下：</p>
<p><img src="http://blog.zhourunsheng.com/wp-content/uploads/2012/05/faplayer_pause.png" alt="faplayer" width="426" height="229" /><br />
<span id="more-1582"></span></p>
<h2>环境配置</h2>
<ul>
<li>Android SDk 参见 <a href="http://developer.android.com/sdk/index.html">http://developer.android.com/sdk/index.html</a></li>
<li>Android NDk 参见 <a href="http://developer.android.com/sdk/ndk/index.html">http://developer.android.com/sdk/ndk/index.html</a></li>
<li>环境变量配置如下（~/.bashrc）：</li>
</ul>
<pre>   export ANDROID_SDK=/home/careychow/android/sdk/android-sdk-linux
   # export ANDROID_NDK=/home/careychow/android/ndk/android-ndk-r8
   export ANDROID_NDK=/home/careychow/android/ndk/android-ndk-r5c
   export PATH=$PATH:$ANDROID_SDK/tools:$ANDROID_SDK/platform-toos:$ANDROID_NDK</pre>
<p><strong>注</strong>： faplayer 利用 android-ndk-r5c 编译，用 android-ndk-r8 编译会出现错误</p>
<ul>
<li>安装ruby环境，推荐安装ruby1.9.2</li>
</ul>
<pre>sudo apt-get install ruby</pre>
<h2>手机CPU类型检测</h2>
<p>我自己的三星s5830i cpu 信息如下</p>
<pre>    C:\Users\Administrator&gt;adb shell cat /proc/cpuinfo
    Processor       : ARMv6-compatible processor rev 5 (v6l)
    BogoMIPS        : 832.90
    Features        : swp half thumb fastmult vfp edsp java
    CPU implementer : 0x41
    CPU architecture: 6TEJ
    CPU variant     : 0x1
    CPU part        : 0xb36
    CPU revision    : 5

    Hardware        : BCM21553 ThunderbirdEDN31 platform
    Revision        : 0000
    Serial          : 0000000000000000</pre>
<h2>编译 faplayer</h2>
<ul>
<li>下载代码
<pre> git clone https://github.com/tewilove/faplayer.git</pre>
</li>
<li>配置 local.properties 指向android sdk的安装路径，我的配置是：
<pre> sdk.dir=/home/careychow/android/sdk/android-sdk-linux</pre>
</li>
<li>配置编译参数并编译代码，具体的配置需要参照上面的cpu信息，如果类型不匹配，编译出来的apk是无法正常安装的，会出现 <strong>Failure [INSTALL_FAILED_CPU_ABI_INCOMPATIBLE]</strong>错误，我的手机不支持neon，所以采用第二个配置参数 ABI=armeabi，可正常安装和执行
<pre>Build examples:
./build.sh # &lt;-- this will build for cortex-a8 with neon by default.
./build.sh ABI=armeabi # &lt;-- this will build for arm1136j-s with softfp.
./build.sh ABI=armeabi-v7a FPU=vfpv3-d16 TUNE=cortex-a9 # &lt;-- Xoom</pre>
</li>
<li>生成 APK
<pre> ant debug</pre>
<p>命令后会在bin文件夹下面生成 faplayer-debug.apk<br />
<strong>注</strong>：如果提示build.xml文件过期，则可用如下命令修复</p>
<pre> android update project -p .</pre>
</li>
</ul>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/05/%e7%bc%96%e8%af%91-faplayer-%e6%92%ad%e6%94%be%e5%99%a8/">编译 faplayer 播放器</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/05/%e7%bc%96%e8%af%91-faplayer-%e6%92%ad%e6%94%be%e5%99%a8/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Android 视频播放之流媒体解析</title>
		<link>http://blog.zhourunsheng.com/2012/05/android-%e8%a7%86%e9%a2%91%e6%92%ad%e6%94%be%e4%b9%8b%e6%b5%81%e5%aa%92%e4%bd%93%e8%a7%a3%e6%9e%90/</link>
		<comments>http://blog.zhourunsheng.com/2012/05/android-%e8%a7%86%e9%a2%91%e6%92%ad%e6%94%be%e4%b9%8b%e6%b5%81%e5%aa%92%e4%bd%93%e8%a7%a3%e6%9e%90/#comments</comments>
		<pubDate>Tue, 22 May 2012 08:04:15 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[mp4]]></category>
		<category><![CDATA[流媒体]]></category>
		<category><![CDATA[视频]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1567</guid>
		<description><![CDATA[<p>最近做的一个项目需求是要实现视频的边缓存边播放,还要能实现视频内部的跳转，实际来讲就是视频点播，只不过要通过h [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/05/android-%e8%a7%86%e9%a2%91%e6%92%ad%e6%94%be%e4%b9%8b%e6%b5%81%e5%aa%92%e4%bd%93%e8%a7%a3%e6%9e%90/">Android 视频播放之流媒体解析</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>最近做的一个项目需求是要实现视频的边缓存边播放,还要能实现视频内部的跳转，实际来讲就是视频点播，只不过要通过http协议来实现，也即用户可以拖动seekbar跳转到播放指定时间点的视频,要实现这样的功能，那么就需要提前解析视频的关键信息，得到时间点对应的视频流的内部偏移量，这样当进行实际跳转的时候就能根据跳转的时间点转换到文件内部的偏移量，进而进行视频流内容的缓存下载。</p>
<p>至于如何实现视频的边缓存边播放，可以参照我的博文《<a href="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视频播放之边缓存边播放</a>》和 《<a href="http://blog.zhourunsheng.com/2012/05/android-%e8%a7%86%e9%a2%91%e6%92%ad%e6%94%be%e4%b9%8b%e6%b5%81%e5%aa%92%e4%bd%93%e6%a0%bc%e5%bc%8f%e5%a4%84%e7%90%86/">Android 视频播放之流媒体格式处理</a>》，本文的目的是给大家展示怎么样进行视频数据流的解析.<span id="more-1567"></span></p>
<h2>前期准备</h2>
<p>依赖的第三方解析包 mp4parser，Google的项目地址为<a href=" http://code.google.com/p/mp4parser/"> http://code.google.com/p/mp4parser/</a>,它主要实现了mp4和其他格式媒体数据的解析，本文的解析代码就是依赖于它底层的数据解析,项目中有一个图形化界面的解析器，下载地址为 <a href="http://mp4parser.googlecode.com/files/isoviewer-2.0-RC-7.jar">http://mp4parser.googlecode.com/files/isoviewer-2.0-RC-7.jar</a>，可以通过它来查看视频文件的整体编码结构。</p>
<p>通过在命令行执行</p>
<pre>F:\workspace\mp4parser-read-only&gt;java -jar isoviewer-2.0-RC-7.jar</pre>
<p>来启动程序，然后通过菜单的File导入需要解析的视频文件就好了</p>
<p>截图如下：<br />
<img class="alignnone" title="parse_mp4" src="http://blog.zhourunsheng.com/wp-content/uploads/2012/05/parse_mp4.png" alt="parse_mp4" width="600" height="400" /></p>
<p>本文代码依赖的jar包如下：</p>
<ol>
<li><a href="http://carey-blog-image.googlecode.com/files/aspectjrt.jar">http://carey-blog-image.googlecode.com/files/aspectjrt.jar</a></li>
<li><a href="http://carey-blog-image.googlecode.com/files/isoparser-1.0-RC-6-20120510.091755-1.jar">http://carey-blog-image.googlecode.com/files/isoparser-1.0-RC-6-20120510.091755-1.jar</a></li>
</ol>
<h2>关键代码解析</h2>
<p><strong>1. 计算视频的总时长</strong></p>
<p>视频的总时间轴 Duration 除以时间轴的比例 Timescale，结果就是当前视频的总秒数</p>
<pre>lengthInSeconds = (double) isoFile.getMovieBox().getMovieHeaderBox().getDuration() / isoFile.getMovieBox().getMovieHeaderBox().getTimescale();</pre>
<p><strong>2. 获取音频和视频的box数据</strong></p>
<p>一般的流媒体数据有两个TrackBox，第一个为音频，第二个为视频</p>
<pre>List&lt;TrackBox&gt; trackBoxes = isoFile.getMovieBox().getBoxes(TrackBox.class);</pre>
<p><strong>3. 解析视频的box数据</strong></p>
<p>取得关键帧的信息，因为视频的跳转只能在关键帧之间跳转，所以我们需要重点关注关键帧对应的时间点</p>
<pre>Iterator&lt;TrackBox&gt; iterator = trackBoxes.iterator();

SampleTableBox stbl = null;
TrackBox trackBox = null;
while (iterator.hasNext()) {
  trackBox = iterator.next();
  stbl = trackBox.getMediaBox().getMediaInformationBox().getSampleTableBox();
  // 我们只处理视频的关键帧，音频数据略过
  if (stbl.getSyncSampleBox() != null) {
    syncSamples = stbl.getSyncSampleBox().getSampleNumber();
    syncSamplesSize = new long[syncSamples.length];
    syncSamplesOffset = new long[syncSamples.length];
    timeOfSyncSamples = new double[syncSamples.length];
    break;
  }
}</pre>
<p><strong>4. 获取视频box中内部的详细参数</strong></p>
<p>包括帧与时间的对应关系，即每一帧对应视频的时间点是第几秒，帧与chunk的对应关系，mp4为了进行视频的压缩，都是把一系列连续的帧放到一个chunk中，每个chunk中帧的数量不等，chunk与数据流文件偏移的关系，即每个chunk对应的视频流数据在文件的具体偏移位置。</p>
<pre>// first we get all sample from the 'normal' MP4 part. if there are none - no problem.
SampleSizeBox sampleSizeBox = trackBox.getSampleTableBox().getSampleSizeBox();
ChunkOffsetBox chunkOffsetBox = trackBox.getSampleTableBox().getChunkOffsetBox();
SampleToChunkBox sampleToChunkBox = trackBox.getSampleTableBox().getSampleToChunkBox();</pre>
<p><strong>5. 计算偏移量 </strong></p>
<p>根据视频的上述信息计算每一个帧对应的具体时间点，以及其数据流在文件中的偏移量</p>
<pre>if (sampleToChunkBox != null
&amp;&amp; sampleToChunkBox.getEntries().size() &gt; 0
&amp;&amp; chunkOffsetBox != null
&amp;&amp; chunkOffsetBox.getChunkOffsets().length &gt; 0
&amp;&amp; sampleSizeBox != null &amp;&amp; sampleSizeBox.getSampleCount() &gt; 0) {
long[] numberOfSamplesInChunk = sampleToChunkBox.blowup(chunkOffsetBox.getChunkOffsets().length);
if (sampleSizeBox.getSampleSize() &gt; 0) {
// Every sample has the same size! no need to store each size separately
// this happens when people use raw audio formats in MP4 (are you stupid guys???)
int sampleIndex = 0;
long sampleSize = sampleSizeBox.getSampleSize();
for (int i = 0; i &lt; numberOfSamplesInChunk.length; i++) {
long thisChunksNumberOfSamples = numberOfSamplesInChunk[i];
long sampleOffset = chunkOffsetBox.getChunkOffsets()[i];
for (int j = 0; j &lt; thisChunksNumberOfSamples; j++) {
   // samples always start with 1 but we start with zero
   // therefore +1
   int syncSamplesIndex = Arrays.binarySearch(syncSamples,sampleIndex + 1);
   if (syncSamplesIndex &gt;= 0) {
      syncSamplesOffset[syncSamplesIndex] = sampleOffset;
      syncSamplesSize[syncSamplesIndex] = sampleSize;
   }
  sampleOffset += sampleSize;
  sampleIndex++;
}
}
} else {
// the normal case where all samples have different sizes
int sampleIndex = 0;
long sampleSizes[] = sampleSizeBox.getSampleSizes();
for (int i = 0; i &lt; numberOfSamplesInChunk.length; i++) {
   long thisChunksNumberOfSamples = numberOfSamplesInChunk[i];
   long sampleOffset = chunkOffsetBox.getChunkOffsets()[i];
   for (int j = 0; j &lt; thisChunksNumberOfSamples; j++) {
      long sampleSize = sampleSizes[sampleIndex];
      // samples always start with 1 but we start with zero therefore +1
      int syncSamplesIndex = Arrays.binarySearch(syncSamples, sampleIndex + 1);
      if (syncSamplesIndex &gt;= 0) {
         syncSamplesOffset[syncSamplesIndex] = sampleOffset;
         syncSamplesSize[syncSamplesIndex] = sampleSize;
      }
      sampleOffset += sampleSize;
      sampleIndex++;
   }
}
}

MediaHeaderBox mdhd = trackBox.getMediaBox().getMediaHeaderBox();
TrackHeaderBox tkhd = trackBox.getTrackHeaderBox();

trackMetaData.setTrackId(tkhd.getTrackId());
trackMetaData.setCreationTime(DateHelper.convert(mdhd.getCreationTime()));
trackMetaData.setLanguage(mdhd.getLanguage());
trackMetaData.setModificationTime(DateHelper.convert(mdhd.getModificationTime()));
trackMetaData.setTimescale(mdhd.getTimescale());
trackMetaData.setHeight(tkhd.getHeight());
trackMetaData.setWidth(tkhd.getWidth());
trackMetaData.setLayer(tkhd.getLayer());

List&lt;TimeToSampleBox.Entry&gt; decodingTimeEntries = null;
if (trackBox.getParent().getBoxes(MovieExtendsBox.class).size() &gt; 0) {
    decodingTimeEntries = new LinkedList&lt;TimeToSampleBox.Entry&gt;();
    for (MovieFragmentBox movieFragmentBox : trackBox.getIsoFile().getBoxes(MovieFragmentBox.class)) {
     List&lt;TrackFragmentBox&gt; trafs = movieFragmentBox.getBoxes(TrackFragmentBox.class);
       for (TrackFragmentBox traf : trafs) {
        if (traf.getTrackFragmentHeaderBox().getTrackId() == trackBox.getTrackHeaderBox().getTrackId()) {
          List&lt;TrackRunBox&gt; truns = traf.getBoxes(TrackRunBox.class);
          for (TrackRunBox trun : truns) {
             for (TrackRunBox.Entry entry : trun.getEntries()) {
               if (trun.isSampleDurationPresent()) {
                 if (decodingTimeEntries.size() == 0 || decodingTimeEntries.get(decodingTimeEntries.size() - 1).getDelta() != entry.getSampleDuration()) {
                    decodingTimeEntries.add(new TimeToSampleBox.Entry(1,entry.getSampleDuration()));
                 } else {
                    TimeToSampleBox.Entry e = decodingTimeEntries.get(decodingTimeEntries.size() - 1);
                    e.setCount(e.getCount() + 1);
               }
            }
         }
      }
    }
  }
}
} else {
    decodingTimeEntries = stbl.getTimeToSampleBox().getEntries();
}

long currentSample = 0;
double currentTime = 0;
for (TimeToSampleBox.Entry entry : decodingTimeEntries) {
   for (int j = 0; j &lt; entry.getCount(); j++) {
      int syncSamplesIndex = Arrays.binarySearch(syncSamples,currentSample + 1);
      if (syncSamplesIndex &gt;= 0) {
        // samples always start with 1 but we start with zero therefore +1
        timeOfSyncSamples[syncSamplesIndex] = currentTime;
   }
  currentTime += (double) entry.getDelta() / (double) trackMetaData.getTimescale();
  currentSample++;
 }
}
}</pre>
<p><strong>6. 在android中运行</strong></p>
<pre>public void printinfo() {
System.out.println("视频总时长(秒): " + lengthInSeconds);
System.out.println("关键帧 \t 帧偏移 \t 帧大小 \t 帧对应的时间");

int size = syncSamples.length;

for (int i = 0; i &lt; size; i++) {
   System.out.println(syncSamples[i] + " " + syncSamplesOffset[i] + " " + syncSamplesSize[i] + " " + timeOfSyncSamples[i]);
}
}</pre>
<p>解析的结果如下：</p>
<pre>05-22 15:21:03.750: I/System.out(5291): remoteUrl: http://bbfile.b0.upaiyun.com/data/videos/2/vid_20120510_090204.mp4
05-22 15:21:03.757: I/System.out(5291): localUrl: /mnt/sdcard/VideoCache/1337671263394.mp4
05-22 15:21:16.343: I/System.out(5291): 视频总时长(秒): 123.636
05-22 15:21:16.343: I/System.out(5291): 关键帧 帧偏移 帧大小 帧对应的时间
05-22 15:21:16.343: I/System.out(5291): 1 405032 1449 0.0
05-22 15:21:16.343: I/System.out(5291): 16 423325 3058 0.5002777777777777
05-22 15:21:16.351: I/System.out(5291): 31 441936 2886 1.0
05-22 15:21:16.351: I/System.out(5291): 46 467978 3575 1.4996777777777774
05-22 15:21:16.351: I/System.out(5291): 61 498237 5578 1.9993999999999998
05-22 15:21:16.351: I/System.out(5291): 76 562086 7654 2.4993444444444437
。。。。。。
05-22 15:21:16.585: I/System.out(5291): 3676 15952210 13153 122.43723333333335
05-22 15:21:16.585: I/System.out(5291): 3691 16050289 11484 122.93692222222225
05-22 15:21:16.585: I/System.out(5291): 3706 16131270 12718 123.4366666666667</pre>
<p>通过以上数据可以分析出：<br />
a. 视频的总时长为 123.636s<br />
b. 1s钟时长的视频大约有30个帧，其中有2个关键帧，就拿第1s的视频数据来说，第1和16帧为关键帧，<br />
其对应的时间点分别为0s和0.5s，帧对应的数据流在当前视频文件内部的偏移量分别为 405032<br />
和 423325，视频流数据的大小分别为1449和3058</p>
<p>源代码文件下载 <a href="http://carey-blog-image.googlecode.com/files/CareyMp4Parser%2820120522%29.java">http://carey-blog-image.googlecode.com/files/CareyMp4Parser%2820120522%29.java</a></p>
<p>好了，视频的解析部分就讲到这里，有什么问题可以给我留言，后续再更新跳转实现的代码部分.</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/05/android-%e8%a7%86%e9%a2%91%e6%92%ad%e6%94%be%e4%b9%8b%e6%b5%81%e5%aa%92%e4%bd%93%e8%a7%a3%e6%9e%90/">Android 视频播放之流媒体解析</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/05/android-%e8%a7%86%e9%a2%91%e6%92%ad%e6%94%be%e4%b9%8b%e6%b5%81%e5%aa%92%e4%bd%93%e8%a7%a3%e6%9e%90/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
<enclosure url="http://bbfile.b0.upaiyun.com/data/videos/2/vid_20120510_090204.mp4" length="16168052" type="video/mp4" />
		</item>
		<item>
		<title>Android 视频播放之流媒体格式处理</title>
		<link>http://blog.zhourunsheng.com/2012/05/android-%e8%a7%86%e9%a2%91%e6%92%ad%e6%94%be%e4%b9%8b%e6%b5%81%e5%aa%92%e4%bd%93%e6%a0%bc%e5%bc%8f%e5%a4%84%e7%90%86/</link>
		<comments>http://blog.zhourunsheng.com/2012/05/android-%e8%a7%86%e9%a2%91%e6%92%ad%e6%94%be%e4%b9%8b%e6%b5%81%e5%aa%92%e4%bd%93%e6%a0%bc%e5%bc%8f%e5%a4%84%e7%90%86/#comments</comments>
		<pubDate>Sat, 19 May 2012 08:13:59 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[mp4]]></category>
		<category><![CDATA[流媒体]]></category>
		<category><![CDATA[视频]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1552</guid>
		<description><![CDATA[<p>格式编码 项目的需求要实现流媒体的播放，简言之就是视频可以支持边下载边播放。鉴于MP4/h.264编码的优势， [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/05/android-%e8%a7%86%e9%a2%91%e6%92%ad%e6%94%be%e4%b9%8b%e6%b5%81%e5%aa%92%e4%bd%93%e6%a0%bc%e5%bc%8f%e5%a4%84%e7%90%86/">Android 视频播放之流媒体格式处理</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<h2>格式编码</h2>
<p>项目的需求要实现流媒体的播放，简言之就是视频可以支持边下载边播放。鉴于MP4/h.264编码的优势，视频采用MP4文件格式存储，因为mp4只是一个存储的规范，像国内的优酷主要采用flv文件格式，其内部也是MP4编码，flv是 Adobe 支持的格式，这样的话，不论是在网站端，通过pc浏览器浏览，借助于<a href="http://get.adobe.com/flashplayer/">Flash Player</a>，还是手持设备，借助于<a href="http://www.ffmpeg.org/">ffmpeg</a>的解码库，都能实现视频的无缝播放，算是一个比较完美的解决方案。<span id="more-1552"></span></p>
<p>在实际视频的处理中，发现一个问题，利用Android手机自己拍摄录制的视频，可以完美支持边下载边播放，通过Iphone手机录制的视频，然后转码成mp4格式的视频就不能支持边下载边播放了，问题困扰了两三天，没办法，开始研究mp4的存储格式，最后终于找到了问题的所在。</p>
<p>了解mp4格式的应该知道，mp4是采用一种box数据结构来存储视频数据的，理想的情况是，box的顺序为ftyp，moov，free和mdat，通过moov的解析就能获取该视频的关键信息，比如播放时长，关键帧数，sample存储情况，在mdat中对应的编码数据偏移量等等。这样只要视频缓存完头部信息，即moov之后，就能知道当前视频的关键数据了，随着mdat的逐渐下载，就能完美实现视频的边缓存边播放，具体的实例教程请参见我的博文<a href="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视频播放之边缓存边播放》</a>。</p>
<p>但是通过第三方软件的编码转换，有的就把moov放置到mdat之后了，这样只有把视频全部下载完成之后才能获取该视频的关键信息，才能开始播放，理论上也就没法实现视频的边缓存边播放，找到问题的关键点之后，就开始寻找解决办法，终于找到qt-faststart，它能解析原视频格式，并将moov信息尽可能的提前，这样就能及早解析和播放视频了。</p>
<h2>格式转换</h2>
<p>下载地址：</p>
<ul>
<li><a href="http://notboring.org/downloads/video/qt-faststart.zip">qt-faststart.exe binary for Windows</a></li>
<li><a href="https://github.com/danielgtaylor/qtfaststart">qt-faststart written in Python</a></li>
<li><a href="http://renaun.com/blog/code/qtindexswapper/">qtindexswapper written for Adobe AIR</a></li>
</ul>
<p>我使用的是window的客户端，使用命令如下：</p>
<pre>Usage: qt-faststart （转换前的视频文件） (转换后的视频文件)</pre>
<p><strong>参考文章</strong></p>
<ul>
<li><a href="http://www.stoimen.com/blog/2010/11/12/how-to-make-mp4-progressive-with-qt-faststart/">how-to-make-mp4-progressive-with-qt-faststart</a></li>
<li><a href="http://multimedia.cx/eggs/improving-qt-faststart/">improving-qt-faststart</a></li>
<li><a href="http://www.chinaunicom.com.cn/upload/1287474923316.pdf">移动流媒体协议综述</a></li>
</ul>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/05/android-%e8%a7%86%e9%a2%91%e6%92%ad%e6%94%be%e4%b9%8b%e6%b5%81%e5%aa%92%e4%bd%93%e6%a0%bc%e5%bc%8f%e5%a4%84%e7%90%86/">Android 视频播放之流媒体格式处理</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/05/android-%e8%a7%86%e9%a2%91%e6%92%ad%e6%94%be%e4%b9%8b%e6%b5%81%e5%aa%92%e4%bd%93%e6%a0%bc%e5%bc%8f%e5%a4%84%e7%90%86/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Android视频播放之边缓存边播放</title>
		<link>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/</link>
		<comments>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/#comments</comments>
		<pubDate>Sun, 13 May 2012 09:23:00 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[缓存]]></category>
		<category><![CDATA[视频]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1528</guid>
		<description><![CDATA[<p>最近在做Android视频播放的有关项目，其中有一项需求就是要求视频可以边加载缓存边播放，类似于优酷土豆的视频 [&#8230;]</p>
<p><a rel="nofollow" href="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视频播放之边缓存边播放</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>最近在做Android视频播放的有关项目，其中有一项需求就是要求视频可以边加载缓存边播放，类似于优酷土豆的视频点播。网上找了一些相关的资料，比较了每种视频格式的优缺点之后，结合Android手机自身的优势，默认支持mp4编码和解码，最终采用mp4格式作为视频的存储格式。</p>
<p>其实最真实的流媒体协议传输格式并不是普通的http方式，而是rtsp，那样的话得搭建专门的流媒体服务器，成本比较高，采用普通的http方式，实现的是一种伪流媒体传输，但是对于常用的视频缓存播放也足够了。</p>
<p>要想实现视频的边缓存边播放，原则上就要求视频的存储格式是分段的，而mp4正好满足这个要求，只要将mp4的整体视频信息放在mp4文件的开头，这样只要加载了mp4文件的头部之后，就能解析出该mp4文件的时长，比特率等等，为后续的视频缓存做初始化设置，然后每加载一段mp4文件的数据流，通过解析头部来或得当前视频流的帧信息，并在播放器中播放，这样就能先加载一段进行播放，同时缓存后续的一段，依此原理就能实现。<span id="more-1528"></span></p>
<p>本文的目的就是给大家介绍一种以此原理而开发一个Android视频边缓存边播放的示例，通过该示例的学习，相信大家能对该原理有更深入的理解。</p>
<p>在介绍具体的demo之前，先放上几张截图，分别为视频播放前的缓存，视频边缓存边播放，缓存完毕的视频播放。</p>
<p><img src="http://blog.zhourunsheng.com/wp-content/uploads/2012/05/jiazaiqian.png" alt="加载前" width="450px" height="260px" /><br />
<img src="http://blog.zhourunsheng.com/wp-content/uploads/2012/05/bofangzhong.png" alt="边缓存边播放" width="450px" height="260px" /><br />
<img src="http://blog.zhourunsheng.com/wp-content/uploads/2012/05/jiazaiwanbi.png" alt="缓存完毕的继续播放" width="450px" height="260px" /></p>
<p><strong>代码解析</strong><br />
VideoViewDemo.java 主要是用来设置启动参数，设定网络视频的url地址和本地缓存的地址，本地缓存的地址可以不设置，程序会自己维护，如果您自己设置了，视频就会缓存到该位置。</p>
<pre>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);
}

}</pre>
<p>BBVideoPlayer.java 就是视频缓存的核心了，READY<em>BUFF定义了初始缓存区的大小，当视频加载到初始缓存区满的时候，播放器开始播放，CACHE</em>BUFF则是核心交换缓存区，主要是用来动态调节缓存区，当网络环境较好的时候，该缓存区为初始大小，当网络环境差的时候，该缓存区会动态增加，主要就是为了避免视频播放的时候出现一卡一卡的现象。</p>
<pre>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) &gt; READY_BUFF) {
                            lastReadSize = readSize;
                            mHandler.sendEmptyMessage(CACHE_VIDEO_READY);
                        }
                    } else {
                        if ((readSize - lastReadSize) &gt; 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);
    }
};
}</pre>
<p>总体来说，原理比较简单，就是把视频加载了一段后，就送到播放器播放，如果出现了错误，则优先缓存一部分文件，然后再继续播放，类似的处理过程循环往复。</p>
<p>程序源代码下载： <a title="code" href="http://carey-blog-image.googlecode.com/files/VideoViewDemo.zip">code</a></p>
<p>大家可以在实际环境中做测试，和依据实际情况动态维护缓存区，在具体的使用过程中有神马问题留言即可！</p>
<p><a rel="nofollow" href="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视频播放之边缓存边播放</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>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/feed/</wfw:commentRss>
		<slash:comments>32</slash:comments>
<enclosure url="http://carey-blog-image.googlecode.com/files/vid_20120510_090204.mp4" length="960" type="video/mp4" />
<enclosure url="http://bbfile.b0.upaiyun.com/data/videos/2/vid_20120510_090204.mp4" length="16168052" type="video/mp4" />
		</item>
	</channel>
</rss>
