<?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/%e7%bc%93%e5%ad%98/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>FLV格式流媒体的缓存渐进拖放原理</title>
		<link>http://blog.zhourunsheng.com/2012/06/flv%e6%a0%bc%e5%bc%8f%e6%b5%81%e5%aa%92%e4%bd%93%e7%9a%84%e7%bc%93%e5%ad%98%e6%b8%90%e8%bf%9b%e6%8b%96%e6%94%be%e5%8e%9f%e7%90%86/</link>
		<comments>http://blog.zhourunsheng.com/2012/06/flv%e6%a0%bc%e5%bc%8f%e6%b5%81%e5%aa%92%e4%bd%93%e7%9a%84%e7%bc%93%e5%ad%98%e6%b8%90%e8%bf%9b%e6%8b%96%e6%94%be%e5%8e%9f%e7%90%86/#comments</comments>
		<pubDate>Sat, 02 Jun 2012 12:21:23 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[Flv]]></category>
		<category><![CDATA[流媒体]]></category>
		<category><![CDATA[缓存]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1620</guid>
		<description><![CDATA[<p>前几天讲解了mp4格式文件的解析，边缓存边播放，拖动处理，faplayer的编译等等，可以查看我的《Andro [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/06/flv%e6%a0%bc%e5%bc%8f%e6%b5%81%e5%aa%92%e4%bd%93%e7%9a%84%e7%bc%93%e5%ad%98%e6%b8%90%e8%bf%9b%e6%8b%96%e6%94%be%e5%8e%9f%e7%90%86/">FLV格式流媒体的缓存渐进拖放原理</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>前几天讲解了mp4格式文件的解析，边缓存边播放，拖动处理，faplayer的编译等等，可以查看我的《<a href="http://blog.zhourunsheng.com/tag/android/">Android系列专题文章</a>》，本文给大家介绍flv格式文件的缓存渐进拖放原理。</p>
<p>flv是Adobe公司提出来的一种视频存储格式，因为其压缩的效果比较好，容量小，所以很适合网络传播，这也就是国外的youtube，国内的优酷，土豆等等都无一类外的采用flv格式来进行存储，有关flv格式的具体内容可以参照<a href="http://en.wikipedia.org/wiki/Flash_Video">Flash Video</a>和 <a href="http://osflash.org/flv">osflash</a>。<span id="more-1620"></span></p>
<p>flv是采用一种tag的数据结构进行流媒体数据的存储，首先是文件头，接下来是可选的meta信息，再后面就是音频和视频数据的交替存储，比起来mp4文件庞大的文件头，flv的文件头基本可以忽略了，只有9个字节，meta部分存储一些音视频解码和长度等的一些信息，具体的音频和视频的tab部分存储实际的流媒体数据。</p>
<p>mp4文件之所以能边缓存边播放，并且可以支持跳转，原因是文件头存储了视频整体的信息，像视频的总时长，关键帧，对应的时间轴，对应的文件偏移量等等，有了这些信息，就能实现在视频全部下载完毕之前就支持跳转播放，如果mp4的文件头放在了文件末端，那么该文件就不支持缓存跳转播放了，只有等待文件全部下载完毕之后才能进行播放，具体怎么样将文件头重新提取到头部，我博客专题的文章中有写，可以参照。</p>
<p>同理，flv文件的meta信息是可选的，如果这部分数据没有存储关键帧，时间轴和文件偏移量，那么该flv文件在全部缓存完毕之前是不支持跳转播放的，那么，大家可能会很奇怪，优酷，土豆看了这么久，所有的视频都是可以拖动跳转的，那么具体的原因是什么，下面我给大家详细解释。</p>
<p>首先大家先了解下什么是flv meta 注入，意思就是通过解析当前flv文件的音视频tag，然后根据这些流媒体数据来生成关键帧，时间轴和偏移量的信息，然后将这些信息重置到该flv文件的meta部分，这样就能够使不支持拖动播放的flv文件变得支持拖动播放了，实际上优酷的做法就是这样，用户上传的视频动态转码成flv格式，并且通过扫描和解析flv文件，进行meta注入，最后我们在网上播放的视频就是处理过的视频。实现flv注入，有两个工具可以使用，具体可以参照<a href="http://www.buraks.com/flvmdi/">flvmdi</a>和<a href="http://www.inlet-media.de/flvtool2/">FLVTool2</a>。</p>
<p>接下来我用win版本的flvtool来分析一段优酷的视频（<a href="http://v.youku.com/v_show/id_XMzk2NzQ1MDUy.html">谢霆锋热血微电影</a>），解析到的meta信息如下：</p>
<pre>---
C:/Users/Administrator/Desktop/flvtool2.exe -P test.flv
  hasKeyframes: true
  hasVideo: true
  framerate: 15
  audiosamplerate: 22050
  hasAudio: true
  height: 288
  metadatacreator: modified by youku.com in 20111202
  keyframes: 
    times: 
      - 0
      - 0
      - 6
      - 12
      - 18
      - 24
      - 25.6666666666667
      - 26.7333333333333
      - 28.3333333333333
      - 30.2666666666667
      // 省略部分
      - 193.866666666667
      - 199.866666666667
      - 205.866666666667
    filepositions: 
      - 1400
      - 1494
      - 98389
      - 205673
      - 312032
      - 422862
      - 438242
      - 495063
      - 555999
      - 695882
      // 省略部分
      - 7259084
      - 7426324
      - 7538447
  duration: 210.066666666667
  hasMetadata: true
  width: 512
...</pre>
<p>通过分析上面的meta信息，我们可以得到该视频的播放尺寸为512*288，视频的总时长约为210s，也就是3分半钟，从keyframes中可以解析得到关键帧对应的时间轴和对应的文件文件偏移量，有了这些信息，就能让该视频支持缓存跳转播放了，还有一句话，大家注意“metadatacreator: modified by youku.com in 20111202”，这是优酷在进行视频处理的时候填入的制作者信息。</p>
<p>好了，原理的部分就讲到这里，至于代码的实现，可以先参照我系列文章中mp4播放器边缓存，边播放，拖动缓存的实现代码，后续再做flv部分的完善。</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/06/flv%e6%a0%bc%e5%bc%8f%e6%b5%81%e5%aa%92%e4%bd%93%e7%9a%84%e7%bc%93%e5%ad%98%e6%b8%90%e8%bf%9b%e6%8b%96%e6%94%be%e5%8e%9f%e7%90%86/">FLV格式流媒体的缓存渐进拖放原理</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/06/flv%e6%a0%bc%e5%bc%8f%e6%b5%81%e5%aa%92%e4%bd%93%e7%9a%84%e7%bc%93%e5%ad%98%e6%b8%90%e8%bf%9b%e6%8b%96%e6%94%be%e5%8e%9f%e7%90%86/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%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>
