FFMPEG+SDL2.0流媒体开发3---简易MP4视频播放器,提取MP4的H264视频序列解码并且显示

简介

之前写了一遍提取MP4中的音视频并且解码,这一篇引入SDL2.0来显示解码后的视频序列 实现一个简易的 视频播放器。

我这里用的FFMPEG和SDL2.0都是最新版的 可能网上的资料不是很多,API接口也变了很多,不过大体的思路还是一样的。

分析几个FFMPEG函数

在这之前我们分析几个代码中可能引起疑问的FFMPEG几个函数的源代码,我已经尽我的能力添加了注释,因为实在没有文档可能有的地方也不是很详尽  不过大体还是能看懂的

av_image_alloc (分配图片缓冲区)

我们在FFMPEG中引用了此函数,下面列举的函数都是这个函数里所引用到的 我都 添加了注释  这里注意下面的

[cpp] view plaincopyprint?

  1. pointers 参数是一个指针数组  实际上他在初始化完毕之后会被赋值成连续的内存序列 具体看源代码

[cpp] view plaincopyprint?

  1. int av_image_alloc(uint8_t *pointers[4], int linesizes[4],
  2. int w, int h, enum AVPixelFormat pix_fmt, int align)
  3. {
  4. //获取描述符
  5. const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(pix_fmt);
  6. //
  7. int i, ret;
  8. uint8_t *buf;
  9. //如果不存在描述符那么返回错误
  10. if (!desc)
  11. return AVERROR(EINVAL);
  12. //检测图像宽度 高度
  13. if ((ret = av_image_check_size(w, h, 0, NULL)) < 0)
  14. return ret;
  15. //填充line sizes
  16. if ((ret = av_image_fill_linesizes(linesizes, pix_fmt, align>7 ? FFALIGN(w, 8) : w)) < 0)
  17. return ret;
  18. //初始化0
  19. for (i = 0; i < 4; i++)
  20. linesizes[i] = FFALIGN(linesizes[i], align);
  21. //如果计算的缓冲区尺寸<0
  22. if ((ret = av_image_fill_pointers(pointers, pix_fmt, h, NULL, linesizes)) < 0)
  23. return ret;
  24. //如果失败 重新分配buf
  25. buf = av_malloc(ret + align);
  26. if (!buf)
  27. return AVERROR(ENOMEM);
  28. //再次调用 分配连续缓冲区  赋值给 pointers
  29. if ((ret = av_image_fill_pointers(pointers, pix_fmt, h, buf, linesizes)) < 0) {
  30. //如果分配失败那么释放 缓冲区
  31. av_free(buf);
  32. return ret;
  33. }
  34. //检测像素描述符 AV_PIX_FMT_FLAG_PAL 或AV_PIX_FMT_FLAG_PSEUDOPAL
  35. //Pixel format has a palette in data[1], values are indexes in this palette.
  36. /**
  37. The pixel format is "pseudo-paletted". This means that FFmpeg treats it as
  38. * paletted internally, but the palette is generated by the decoder and is not
  39. * stored in the file. *
  40. */
  41. if (desc->flags & AV_PIX_FMT_FLAG_PAL || desc->flags & AV_PIX_FMT_FLAG_PSEUDOPAL)
  42. //设置系统调色板
  43. avpriv_set_systematic_pal2((uint32_t*)pointers[1], pix_fmt);
  44. return ret;
  45. }

avpriv_set_systematic_pal2(设置系统调色板)

//设置系统化调色板根据不同像素格式

[cpp] view plaincopyprint?

  1. int avpriv_set_systematic_pal2(uint32_t pal[256], enum AVPixelFormat pix_fmt)
  2. {
  3. int i;
  4. for (i = 0; i < 256; i++) {
  5. int r, g, b;
  6. switch (pix_fmt) {
  7. case AV_PIX_FMT_RGB8:
  8. r = (i>>5    )*36;
  9. g = ((i>>2)&7)*36;
  10. b = (i&3     )*85;
  11. break;
  12. case AV_PIX_FMT_BGR8:
  13. b = (i>>6    )*85;
  14. g = ((i>>3)&7)*36;
  15. r = (i&7     )*36;
  16. break;
  17. case AV_PIX_FMT_RGB4_BYTE:
  18. r = (i>>3    )*255;
  19. g = ((i>>1)&3)*85;
  20. b = (i&1     )*255;
  21. break;
  22. case AV_PIX_FMT_BGR4_BYTE:
  23. b = (i>>3    )*255;
  24. g = ((i>>1)&3)*85;
  25. r = (i&1     )*255;
  26. break;
  27. case AV_PIX_FMT_GRAY8:
  28. r = b = g = i;
  29. break;
  30. default:
  31. return AVERROR(EINVAL);
  32. }
  33. pal[i] = b + (g << 8) + (r << 16) + (0xFFU << 24);
  34. }
  35. return 0;
  36. }

av_image_fill_pointers(填充av_image_alloc传递的unsigned char** data和linesize)

[cpp] view plaincopyprint?

  1. //返回图像所需的大小
  2. //并且分配了连续缓冲区  将 data 拼接成一个内存连续的 序列
  3. int av_image_fill_pointers(uint8_t *data[4], enum AVPixelFormat pix_fmt, int height,
  4. uint8_t *ptr, const int linesizes[4])
  5. {
  6. int i, total_size, size[4] = { 0 }, has_plane[4] = { 0 };
  7. //获取描述符
  8. const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(pix_fmt);
  9. //清空指针数组
  10. memset(data  , 0, sizeof(data[0])*4);
  11. //如果不存在描述符 返回错误
  12. if (!desc || desc->flags & AV_PIX_FMT_FLAG_HWACCEL)
  13. return AVERROR(EINVAL);
  14. //data[0]初始化为ptr
  15. data[0] = ptr;
  16. //如果每行的像素 大于INT类型最大值 -1024/高度 返回
  17. if (linesizes[0] > (INT_MAX - 1024) / height)
  18. return AVERROR(EINVAL);
  19. //初始化size[0]
  20. size[0] = linesizes[0] * height;
  21. //如果 描述符的标志是AV_PIX_FMT_FLAG_PAL或者AV_PIX_FMT_FLAG_PSEUDOPAL 那么表明调色板放在data[1]并且是 256 32位置
  22. if (desc->flags & AV_PIX_FMT_FLAG_PAL ||
  23. desc->flags & AV_PIX_FMT_FLAG_PSEUDOPAL)
  24. {
  25. size[0] = (size[0] + 3) & ~3;
  26. data[1] = ptr + size[0];
  27. return size[0] + 256 * 4;
  28. }
  29. /**
  30. * Parameters that describe how pixels are packed.
  31. * If the format has 2 or 4 components, then alpha is last.
  32. * If the format has 1 or 2 components, then luma is 0.
  33. * If the format has 3 or 4 components,
  34. * if the RGB flag is set then 0 is red, 1 is green and 2 is blue;
  35. * otherwise 0 is luma, 1 is chroma-U and 2 is chroma-V.
  36. */
  37. for (i = 0; i < 4; i++)
  38. has_plane[desc->comp[i].plane] = 1;
  39. //下面是计算总的需要的缓冲区大小
  40. total_size = size[0];
  41. for (i = 1; i < 4 && has_plane[i]; i++) {
  42. int h, s = (i == 1 || i == 2) ? desc->log2_chroma_h : 0;
  43. data[i] = data[i-1] + size[i-1];
  44. h = (height + (1 << s) - 1) >> s;
  45. if (linesizes[i] > INT_MAX / h)
  46. return AVERROR(EINVAL);
  47. size[i] = h * linesizes[i];
  48. if (total_size > INT_MAX - size[i])
  49. return AVERROR(EINVAL);
  50. total_size += size[i];
  51. }
  52. //返回总的缓冲区 大小
  53. return total_size;
  54. }

av_image_fill_linesizes(填充行线宽)

[cpp] view plaincopyprint?

  1. //填充LineSize数组 ,linesize代表每一刚的线宽 像素为单位
  2. int av_image_fill_linesizes(int linesizes[4], enum AVPixelFormat pix_fmt, int width)
  3. {
  4. int i, ret;
  5. //获取格式描述符
  6. const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(pix_fmt);
  7. int max_step     [4];       /* max pixel step for each plane */
  8. int max_step_comp[4];       /* the component for each plane which has the max pixel step */
  9. //初始化指针数组 0
  10. memset(linesizes, 0, 4*sizeof(linesizes[0]));
  11. //如果不存在那么返回错误
  12. if (!desc || desc->flags & AV_PIX_FMT_FLAG_HWACCEL)
  13. return AVERROR(EINVAL);
  14. //下面的代码都是填充线宽的代码
  15. av_image_fill_max_pixsteps(max_step, max_step_comp, desc);
  16. for (i = 0; i < 4; i++) {
  17. if ((ret = image_get_linesize(width, i, max_step[i], max_step_comp[i], desc)) < 0)
  18. return ret;
  19. linesizes[i] = ret;
  20. }
  21. return 0;
  22. }

例子 提取MP4文件的视频,并播放实现简易视频播放器

[cpp] view plaincopyprint?

  1. #include "stdafx.h"
  2. /************************************************************************/
  3. /* 利用分流器分流MP4文件音视频并进行解码输出
  4. Programmer小卫-USher 2014/12/17
  5. /************************************************************************/
  6. //打开
  7. #define __STDC_FORMAT_MACROS
  8. #ifdef _CPPRTTI
  9. extern "C"
  10. {
  11. #endif
  12. #include "libavutil/imgutils.h"    //图像工具
  13. #include "libavutil/samplefmt.h"  // 音频样本格式
  14. #include "libavutil/timestamp.h"  //时间戳工具可以 被用于调试和日志目的
  15. #include "libavformat/avformat.h" //Main libavformat public API header  包含了libavf I/O和   Demuxing  和Muxing 库
  16. #include "SDL.h"
  17. #ifdef _CPPRTTI
  18. };
  19. #endif
  20. //音视频编码器上下文
  21. static AVCodecContext *pVideoContext,*pAudioContext;
  22. static FILE *fVideoFile,*fAudioFile;  //输出文件句柄
  23. static AVStream *pStreamVideo,*pStreamAudio; //媒体流
  24. static unsigned char * videoDstData[4];  //视频数据
  25. static int videoLineSize[4]; //
  26. static int videoBufferSize; //视频缓冲区大小
  27. static AVFormatContext *pFormatCtx=NULL; //格式上下文
  28. static AVFrame*pFrame=NULL ; //
  29. static AVPacket pkt;  //解码媒体包
  30. static int ret=0; //状态
  31. static int gotFrame; //获取到的视频流
  32. //音视频流的索引
  33. static int videoStreamIndex,audioStreamIndex;
  34. //解码媒体包
  35. //SDL定义
  36. SDL_Window * pWindow = NULL;
  37. SDL_Renderer *pRender = NULL;
  38. SDL_Texture *pTexture = NULL;
  39. SDL_Rect dstrect = {0,0,800,600};
  40. int frame = 0;
  41. int indexFrameVideo=0;
  42. static int decode_packet(int* gotFrame, int param2)
  43. {
  44. int ret  = 0 ;
  45. //解码数据大小
  46. int decodedSize=pkt.size ;
  47. //初始化获取的数据帧为0
  48. *gotFrame=0;
  49. //如果是视频流那么 解包视频流
  50. if(pkt.stream_index==videoStreamIndex)
  51. {
  52. //解码数据到视频帧
  53. if((ret=avcodec_decode_video2(pVideoContext,pFrame,gotFrame,&pkt))<0)
  54. {
  55. //解码视频帧失败
  56. return ret ;
  57. }
  58. indexFrameVideo++;
  59. //copy 解压后的数据到我们分配的空间中
  60. if(*gotFrame)
  61. {
  62. //拷贝数据
  63. av_image_copy(videoDstData,videoLineSize, (const uint8_t **)(pFrame->data), pFrame->linesize,pVideoContext->pix_fmt, pVideoContext->width, pVideoContext->height);
  64. //写入数据到缓冲区
  65. //fwrite(videoDstData[0], 1, videoBufferSize, fVideoFile);
  66. printf("输出当前第%d帧,大小:%d\n",indexFrameVideo,videoBufferSize);
  67. int n = SDL_BYTESPERPIXEL(pStreamVideo->codec->pix_fmt);
  68. //更新纹理
  69. SDL_UpdateTexture(pTexture, &dstrect, (const void*)videoDstData[0], videoLineSize[0]);
  70. //拷贝纹理到2D模块
  71. SDL_RenderCopy(pRender, pTexture,NULL, &dstrect);
  72. //延时 1000ms*1/25
  73. SDL_Delay(1000 * 1 / frame);
  74. //显示Render渲染曾
  75. SDL_RenderPresent(pRender);
  76. }else
  77. {
  78. printf("第%d帧,丢失\n",indexFrameVideo);
  79. }
  80. }
  81. //音频不管
  82. else if(pkt.stream_index==audioStreamIndex)
  83. {
  84. ///解码音频信息
  85. //      if ((ret = avcodec_decode_audio4(pAudioContext, pFrame, gotFrame, &pkt)) < 0)
  86. //          return ret;
  87. //      decodedSize = FFMIN(ret, pkt.size);
  88. //      //算出当前帧的大小
  89. //      size_t unpadded_linesize = pFrame->nb_samples * av_get_bytes_per_sample((AVSampleFormat)pFrame->format);
  90. //      ///写入数据到音频文件
  91. //      fwrite(pFrame->extended_data[0], 1, unpadded_linesize, fAudioFile);
  92. }
  93. //取消所有引用  并且重置frame字段
  94. av_frame_unref(pFrame);
  95. return decodedSize ;
  96. }
  97. int Demuxing(int argc, char** argv)
  98. {
  99. if (argc < 4)
  100. {
  101. printf("Parameter Error!\n");
  102. return 0;
  103. }
  104. //注册所有混流器 过滤器
  105. av_register_all();
  106. //注册所有编码器
  107. avcodec_register_all();
  108. //媒体输入源头
  109. char*pInputFile = argv[1];
  110. //视频输出文件
  111. char*pOutputVideoFile = argv[3];
  112. //音频输出文件
  113. char*pOutputAudioFile = argv[2];
  114. //分配环境上下文
  115. pFormatCtx = avformat_alloc_context();
  116. //打开输入源  并且读取输入源的头部
  117. if (avformat_open_input(&pFormatCtx, pInputFile, NULL, NULL) < 0)
  118. {
  119. printf("Open Input Error!\n");
  120. return 0;
  121. }
  122. //获取流媒体信息
  123. if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
  124. {
  125. printf("获取流媒体信息失败!\n");
  126. return 0;
  127. }
  128. //打印媒体信息
  129. av_dump_format(pFormatCtx, 0, pInputFile, 0);
  130. for (unsigned i = 0; i < pFormatCtx->nb_streams; i++)
  131. {
  132. AVStream *pStream = pFormatCtx->streams[i];
  133. AVMediaType mediaType = pStream->codec->codec_type;
  134. //提取不同的编解码器
  135. if (mediaType == AVMEDIA_TYPE_VIDEO)
  136. {
  137. videoStreamIndex = i;
  138. pVideoContext = pStream->codec;
  139. pStreamVideo = pStream;
  140. fVideoFile = fopen(pOutputVideoFile, "wb");
  141. frame = pVideoContext->framerate.num;
  142. if (!fVideoFile)
  143. {
  144. printf("con‘t open file!\n");
  145. goto end;
  146. }
  147. //计算解码后一帧图像的大小
  148. //int nsize = avpicture_get_size(PIX_FMT_YUV420P, 1280, 720);
  149. //分配计算初始化 图像缓冲区 调色板数据
  150. int ret = av_image_alloc(videoDstData, videoLineSize, pVideoContext->width, pVideoContext->height, pVideoContext->pix_fmt, 1);
  151. if (ret < 0)
  152. {
  153. printf("Alloc video buffer error!\n");
  154. goto end;
  155. }
  156. //avpicture_fill((AVPicture *)pFrame, videoDstData[0], PIX_FMT_YUV420P, 1280, 720);
  157. videoBufferSize = ret;
  158. }
  159. else if (mediaType == AVMEDIA_TYPE_AUDIO)
  160. {
  161. audioStreamIndex = i;
  162. pAudioContext = pStream->codec;
  163. pStreamAudio = pStream;
  164. fAudioFile = fopen(pOutputAudioFile, "wb");
  165. if (!fAudioFile)
  166. {
  167. printf("con‘t open file!\n");
  168. goto end;
  169. }
  170. //分配视频帧
  171. pFrame = av_frame_alloc();
  172. if (pFrame == NULL)
  173. {
  174. av_freep(&videoDstData[0]);
  175. printf("alloc audio frame error\n");
  176. goto end;
  177. }
  178. }
  179. AVCodec *dec;
  180. //根据编码器id查找编码器
  181. dec = avcodec_find_decoder(pStream->codec->codec_id);
  182. if (dec == NULL)
  183. {
  184. printf("查找编码器失败!\n");
  185. goto end;
  186. }
  187. if (avcodec_open2(pStream->codec, dec, nullptr) != 0)
  188. {
  189. printf("打开编码器失败!\n");
  190. goto end;
  191. }
  192. }
  193. av_init_packet(&pkt);
  194. pkt.data = NULL;
  195. pkt.size = 0;
  196. //读取媒体数据包  数据要大于等于0
  197. while (av_read_frame(pFormatCtx, &pkt) >= 0)
  198. {
  199. AVPacket oriPkt = pkt;
  200. do
  201. {
  202. //返回每个包解码的数据
  203. ret = decode_packet(&gotFrame, 0);
  204. if (ret < 0)
  205. break;
  206. //指针后移  空闲内存减少
  207. pkt.data += ret;
  208. pkt.size -= ret;
  209. //
  210. } while (pkt.size > 0);
  211. //释放之前分配的空间  读取完毕必须释放包
  212. av_free_packet(&oriPkt);
  213. }
  214. end:
  215. //关闭视频编码器
  216. avcodec_close(pVideoContext);
  217. //关闭音频编码器
  218. avcodec_close(pAudioContext);
  219. avformat_close_input(&pFormatCtx);
  220. fclose(fVideoFile);
  221. fclose(fAudioFile);
  222. //释放编码帧
  223. avcodec_free_frame(&pFrame);
  224. //释放视频数据区
  225. av_free(videoDstData[0]);
  226. return 0;
  227. }
  228. int _tmain(int argc, char*argv[])
  229. {
  230. SDL_Init(SDL_INIT_VIDEO);
  231. //创建窗口
  232. pWindow = SDL_CreateWindow("YUV420P", 200, 100, 800, 600, 0);
  233. //启用硬件加速
  234. pRender=SDL_CreateRenderer(pWindow, -1, 0);
  235. dstrect.x = 0;
  236. dstrect.y = 0;
  237. dstrect.w = 1280;
  238. dstrect.h = 720;
  239. //创建一个纹理  设置可以Lock  YUV420P 格式 1280*720
  240. pTexture = SDL_CreateTexture(pRender, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, 1280, 720);
  241. Demuxing(argc, argv);
  242. //释放
  243. SDL_RenderClear(pRender);
  244. SDL_DestroyTexture(pTexture);
  245. SDL_DestroyRenderer(pRender);
  246. SDL_DestroyWindow(pWindow);
  247. SDL_Quit();
  248. return  0;
  249. }

代码运行界面

时间: 2024-08-06 03:20:32

FFMPEG+SDL2.0流媒体开发3---简易MP4视频播放器,提取MP4的H264视频序列解码并且显示的相关文章

[原]如何在Android用FFmpeg+SDL2.0解码图像线程

关于如何在Android上用FFmpeg+SDL2.0解码显示图像参考[原]如何在Android用FFmpeg+SDL2.0解码显示图像 ,关于如何在Android使用FFmpeg+SDL2.0解码声音参考[原]如何在Android用FFmpeg+SDL2.0解码声音.但是该文章有一个问题,就是解码出来的声音有很大的噪音,基本无法听清,这是由于对于声音的处理有问题.故本文参考ffmpeg-sdl音频播放分析声音解码的处理,解码出来的声音就正常了. 博主的开发环境:Ubuntu 14.04 64位

MP4视频播放器代码

mp4网页视频播放器代码: <video id="video" width="100%" src="http://www.mlhd.org/shipin/018.mp4" style="margin: h.264" controls></video> 此代码由河北魅力网络进行整理,能在网页中实现mp4播放,代码能自动加载视频缓冲条,演示娱乐图,默认为不自动播放,保留带宽. 大家在遇到视频播放的问题的时候

iso 开发学习--简易音乐播放器(基于iPhone4s屏幕尺寸)

三个按钮  一个进度条 贴图(软件中部分图片,来自网络,如果侵犯了您的权益,请联系我,会立刻撤下) 核心代码 // // ViewController.m // 08-10-MusicPlayer // // Created by Ibokan on 15/8/10. // Copyright (c) 2015年 Crazy凡. All rights reserved. // #import "ViewController.h" #import <AVFoundation/AVF

【Android 多媒体开发】 MediaPlayer 网络视频播放器

作者 : 万境绝尘 ([email protected]) 转载请著名出处 : http://blog.csdn.net/shulianghan/article/details/38895143 一. 相关模块解析 1. 播放载体 SurfaceView 简介 (1) SurfaceView 与 Surface SurfaceView 与 Surface 简介 : SurfaceView 中嵌入了一个 Surface, SurfaceView 可以操控 Surface 的 位置, 大小尺寸等;

Android进阶:自定义视频播放器开发(上)

随着快手,抖音,西瓜视频等视频APP的崛起,视频播放已经成为主流,此时作为Android研发的你,想要提高自己的能力还不知道怎么开发视频播放器怎么行?所以今天就带着大家一起开发一个简易播放器:SmallVideoPlayer 需求分析 我们观察一个视频播放器,可以看到视频播放器除了正在播放的视频还有很多控件,比如播放按钮,暂停按钮,播放进度条,播放计时器等.这么多控件显然无法播放视频,但是他们都在控制视频的播放.由此可见视频播放器可以分为两层,一层为视频播放器控制层,一层为真正的视频播放层. 所

基于FFMPEG SDK流媒体开发1---解码媒体文件流信息

最近项目涉及到流媒体等开发,由于有过开发经验深知其难度所在,没办法只能重新拾起,最新版的SDK被改的一塌糊涂,不过大体的开发思路都是一样的,看多少书查多少资料都无用,一步一步的编写代码 才是学好的关键.. 我会把每一天的学习经过,更新到博文上,希望能给更多想学习的人带来帮助,篇尾附上工程     以及最新版本SDK. FFMPEG被大多数的人命令行来使用,其实在真正的流媒体开发中,要想灵活运用其开发流媒体应用层序,必须使用官方SDK开发  ,实际上我们市面上好多产品 都是基于FFMPEG,比如

基于FFMPEG SDK流媒体开发1---解码媒体文件流信息(转)

最近项目涉及到流媒体等开发,由于有过开发经验深知其难度所在,没办法只能重新拾起,最新版的SDK被改的一塌糊涂,不过大体的开发思路都是一样的,看多少书查多少资料都无用,一步一步的编写代码 才是学好的关键.. 我会把每一天的学习经过,更新到博文上,希望能给更多想学习的人带来帮助,篇尾附上工程     以及最新版本SDK. FFMPEG被大多数的人命令行来使用,其实在真正的流媒体开发中,要想灵活运用其开发流媒体应用层序,必须使用官方SDK开发  ,实际上我们市面上好多产品 都是基于FFMPEG,比如

基于&lt;最简单的基于FFMPEG+SDL的视频播放器 ver2 (采用SDL2.0)&gt;的一些个人总结

最近因为项目接近收尾阶段,所以变的没有之前那么忙了,所以最近重新拿起了之前的一些FFMPEG和SDL的相关流媒体播放器的例子在看. 同时自己也用FFMPEG2.01,SDL2.01结合MFC以及网上罗列的一些资料,打算打造一款自己的简易播放器. 最先开始是阅读了<An ffmpeg and SDL Tutorial>以及来源与(http://blog.csdn.net/love4mario/article/details/17652355)中的中文资料,同时认真对tutorial01-08中的

演示基于SDL2.0+FFmpeg的播放器

SDL是一个跨平台的渲染组件,目前已经推出到2.0.3版本,支持Win/Linux/OSX/Android.网上很多介绍大多是基于SDL1.2版本的,与2.0版本有一定的差别,本文演示如何用SDL2.0版本播放视频(仅视频). SDL下载网站:http://libsdl.org 参考网址:http://blog.csdn.net/dawdo222/article/details/8692834 上代码: // 演示如何用SDL2进行播放 //可参考http://blog.csdn.net/daw