本文的示例将实现:读取PC摄像头视频数据并以RTMP协议发送为直播流。示例包含了

1、ffmpeg的libavdevice的使用

2、视频解码、编码、推流的基本流程

具有较强的综合性。

要使用libavdevice的相关函数,首先需要注册相关组件

  1. avdevice_register_all();

接下来我们要列出电脑中可用的dshow设备

  1. AVFormatContext *pFmtCtx = avformat_alloc_context();
  2. AVDeviceInfoList *device_info = NULL;
  3. AVDictionary* options = NULL;
  4. av_dict_set(&options, "list_devices", "true", 0);
  5. AVInputFormat *iformat = av_find_input_format("dshow");
  6. printf("Device Info=============\n");
  7. avformat_open_input(&pFmtCtx, "video=dummy", iformat, &options);
  8. printf("========================\n");

可以看到这里打开设备的步骤基本与打开文件的步骤相同,上面的代码中设置了AVDictionary,这样与在命令行中输入下列命令有相同的效果

  1. ffmpeg -list_devices true -f dshow -i dummy

以上语句得到的结果如下

这里我的电脑上只有一个虚拟摄像头软件虚拟出来的几个dshow设备,没有音频设备,所以有如上的结果。

需要说明的是,avdevice有一个avdevice_list_devices函数可以枚举系统的采集设备,包括设备名和设备描述,非常适合用于让用户选择要使用的设备,但是不支持dshow设备,所以这里没有使用它。

下一步就可以像打开普通文件一样将上面的具体设备名作为输入打开,并进行相应的初始化设置,如下

  1. av_register_all();
  2. //Register Device
  3. avdevice_register_all();
  4. avformat_network_init();
  5. //Show Dshow Device
  6. show_dshow_device();
  7. printf("\nChoose capture device: ");
  8. if (gets(capture_name) == 0)
  9. {
  10. printf("Error in gets()\n");
  11. return -1;
  12. }
  13. sprintf(device_name, "video=%s", capture_name);
  14. ifmt=av_find_input_format("dshow");
  15. //Set own video device's name
  16. if (avformat_open_input(&ifmt_ctx, device_name, ifmt, NULL) != 0){
  17. printf("Couldn't open input stream.(无法打开输入流)\n");
  18. return -1;
  19. }
  20. //input initialize
  21. if (avformat_find_stream_info(ifmt_ctx, NULL)<0)
  22. {
  23. printf("Couldn't find stream information.(无法获取流信息)\n");
  24. return -1;
  25. }
  26. videoindex = -1;
  27. for (i = 0; i<ifmt_ctx->nb_streams; i++)
  28. if (ifmt_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
  29. {
  30. videoindex = i;
  31. break;
  32. }
  33. if (videoindex == -1)
  34. {
  35. printf("Couldn't find a video stream.(没有找到视频流)\n");
  36. return -1;
  37. }
  38. if (avcodec_open2(ifmt_ctx->streams[videoindex]->codec, avcodec_find_decoder(ifmt_ctx->streams[videoindex]->codec->codec_id), NULL)<0)
  39. {
  40. printf("Could not open codec.(无法打开解码器)\n");
  41. return -1;
  42. }

在选择了输入设备并进行相关初始化之后,需要对输出做相应的初始化。ffmpeg将网络协议和文件同等看待,同时因为使用RTMP协议进行传输,这里我们指定输出为flv格式,编码器使用H.264

  1. //output initialize
  2. avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", out_path);
  3. //output encoder initialize
  4. pCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
  5. if (!pCodec){
  6. printf("Can not find encoder! (没有找到合适的编码器!)\n");
  7. return -1;
  8. }
  9. pCodecCtx=avcodec_alloc_context3(pCodec);
  10. pCodecCtx->pix_fmt = PIX_FMT_YUV420P;
  11. pCodecCtx->width = ifmt_ctx->streams[videoindex]->codec->width;
  12. pCodecCtx->height = ifmt_ctx->streams[videoindex]->codec->height;
  13. pCodecCtx->time_base.num = 1;
  14. pCodecCtx->time_base.den = 25;
  15. pCodecCtx->bit_rate = 400000;
  16. pCodecCtx->gop_size = 250;
  17. /* Some formats,for example,flv, want stream headers to be separate. */
  18. if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
  19. pCodecCtx->flags |= CODEC_FLAG_GLOBAL_HEADER;
  20. //H264 codec param
  21. //pCodecCtx->me_range = 16;
  22. //pCodecCtx->max_qdiff = 4;
  23. //pCodecCtx->qcompress = 0.6;
  24. pCodecCtx->qmin = 10;
  25. pCodecCtx->qmax = 51;
  26. //Optional Param
  27. pCodecCtx->max_b_frames = 3;
  28. // Set H264 preset and tune
  29. AVDictionary *param = 0;
  30. av_dict_set(&param, "preset", "fast", 0);
  31. av_dict_set(&param, "tune", "zerolatency", 0);
  32. if (avcodec_open2(pCodecCtx, pCodec,&param) < 0){
  33. printf("Failed to open encoder! (编码器打开失败!)\n");
  34. return -1;
  35. }
  36. //Add a new stream to output,should be called by the user before avformat_write_header() for muxing
  37. video_st = avformat_new_stream(ofmt_ctx, pCodec);
  38. if (video_st == NULL){
  39. return -1;
  40. }
  41. video_st->time_base.num = 1;
  42. video_st->time_base.den = 25;
  43. video_st->codec = pCodecCtx;
  44. //Open output URL,set before avformat_write_header() for muxing
  45. if (avio_open(&ofmt_ctx->pb,out_path, AVIO_FLAG_READ_WRITE) < 0){
  46. printf("Failed to open output file! (输出文件打开失败!)\n");
  47. return -1;
  48. }
  49. //Show some Information
  50. av_dump_format(ofmt_ctx, 0, out_path, 1);
  51. //Write File Header
  52. avformat_write_header(ofmt_ctx,NULL);

完成输入和输出的初始化之后,就可以正式开始解码和编码并推流的流程了,这里要注意,摄像头数据往往是RGB格式的,需要将其转换为YUV420P格式,所以要先做如下的准备工作

  1. //prepare before decode and encode
  2. dec_pkt = (AVPacket *)av_malloc(sizeof(AVPacket));
  3. //enc_pkt = (AVPacket *)av_malloc(sizeof(AVPacket));
  4. //camera data has a pix fmt of RGB,convert it to YUV420
  5. img_convert_ctx = sws_getContext(ifmt_ctx->streams[videoindex]->codec->width, ifmt_ctx->streams[videoindex]->codec->height,
  6. ifmt_ctx->streams[videoindex]->codec->pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
  7. pFrameYUV = avcodec_alloc_frame();
  8. uint8_t *out_buffer = (uint8_t *)av_malloc(avpicture_get_size(PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height));
  9. avpicture_fill((AVPicture *)pFrameYUV, out_buffer, PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);

下面就可以正式开始解码、编码和推流了

  1. //start decode and encode
  2. int64_t start_time=av_gettime();
  3. while (av_read_frame(ifmt_ctx, dec_pkt) >= 0){
  4. if (exit_thread)
  5. break;
  6. av_log(NULL, AV_LOG_DEBUG, "Going to reencode the frame\n");
  7. pframe = av_frame_alloc();
  8. if (!pframe) {
  9. ret = AVERROR(ENOMEM);
  10. return -1;
  11. }
  12. //av_packet_rescale_ts(dec_pkt, ifmt_ctx->streams[dec_pkt->stream_index]->time_base,
  13. //  ifmt_ctx->streams[dec_pkt->stream_index]->codec->time_base);
  14. ret = avcodec_decode_video2(ifmt_ctx->streams[dec_pkt->stream_index]->codec, pframe,
  15. &dec_got_frame, dec_pkt);
  16. if (ret < 0) {
  17. av_frame_free(&pframe);
  18. av_log(NULL, AV_LOG_ERROR, "Decoding failed\n");
  19. break;
  20. }
  21. if (dec_got_frame){
  22. sws_scale(img_convert_ctx, (const uint8_t* const*)pframe->data, pframe->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
  23. enc_pkt.data = NULL;
  24. enc_pkt.size = 0;
  25. av_init_packet(&enc_pkt);
  26. ret = avcodec_encode_video2(pCodecCtx, &enc_pkt, pFrameYUV, &enc_got_frame);
  27. av_frame_free(&pframe);
  28. if (enc_got_frame == 1){
  29. //printf("Succeed to encode frame: %5d\tsize:%5d\n", framecnt, enc_pkt.size);
  30. framecnt++;
  31. enc_pkt.stream_index = video_st->index;
  32. //Write PTS
  33. AVRational time_base = ofmt_ctx->streams[videoindex]->time_base;//{ 1, 1000 };
  34. AVRational r_framerate1 = ifmt_ctx->streams[videoindex]->r_frame_rate;// { 50, 2 };
  35. AVRational time_base_q = { 1, AV_TIME_BASE };
  36. //Duration between 2 frames (us)
  37. int64_t calc_duration = (double)(AV_TIME_BASE)*(1 / av_q2d(r_framerate1));  //内部时间戳
  38. //Parameters
  39. //enc_pkt.pts = (double)(framecnt*calc_duration)*(double)(av_q2d(time_base_q)) / (double)(av_q2d(time_base));
  40. enc_pkt.pts = av_rescale_q(framecnt*calc_duration, time_base_q, time_base);
  41. enc_pkt.dts = enc_pkt.pts;
  42. enc_pkt.duration = av_rescale_q(calc_duration, time_base_q, time_base); //(double)(calc_duration)*(double)(av_q2d(time_base_q)) / (double)(av_q2d(time_base));
  43. enc_pkt.pos = -1;
  44. //Delay
  45. int64_t pts_time = av_rescale_q(enc_pkt.dts, time_base, time_base_q);
  46. int64_t now_time = av_gettime() - start_time;
  47. if (pts_time > now_time)
  48. av_usleep(pts_time - now_time);
  49. ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);
  50. av_free_packet(&enc_pkt);
  51. }
  52. }
  53. else {
  54. av_frame_free(&pframe);
  55. }
  56. av_free_packet(dec_pkt);
  57. }

解码部分比较简单,编码部分需要自己计算PTS、DTS,比较复杂。这里通过帧率计算PTS和DTS

首先通过帧率计算每两帧之间的时间间隔,但是要换算为ffmpeg内部的时间基表示的值。所谓ffmpeg内部的时间基即AV_TIME_BASE,定义为

  1. #define         AV_TIME_BASE   1000000

任何以秒为单位的时间值都通过下式转换为ffmpeg内部时间基表示的时间值,其实就是转换为了微秒

  1. timestamp=AV_TIME_BASE*time(s)

所以有

  1. //Duration between 2 frames (us)
  2. int64_t calc_duration = (double)(AV_TIME_BASE)*(1 / av_q2d(r_framerate1));  //内部时间戳

而enc_pkt因为是要写入最后的输出码流的,它的PTS、DTS应该是以ofmt_ctx->streams[videoindex]->time_base为时间基来表示的,时间基之间的转换用下式

  1. enc_pkt.pts = av_rescale_q(framecnt*calc_duration, time_base_q, time_base);

其实就是

  1. enc_pkt.pts = (double)(framecnt*calc_duration)*(double)(av_q2d(time_base_q)) / (double)(av_q2d(time_base));

非常简单的数学转换。

还有一点,因为转码流程可能比实际的播放快很多,为保持流畅的播放,要判断DTS和当前真实时间,并进行相应的延时操作,如下

  1. //Delay
  2. int64_t pts_time = av_rescale_q(enc_pkt.dts, time_base, time_base_q);
  3. int64_t now_time = av_gettime() - start_time;
  4. if (pts_time > now_time)
  5. av_usleep(pts_time - now_time);

这里正好与之前相反,要将ofmt_ctx->streams[videoindex]->time_base时间基转换为ffmpeg内部时间基,因为av_gettime获得的就是以微秒为单位的时间

总体流程完毕之后,还剩下最后的flush encoder操作,输出之前存储在缓冲区内的数据

  1. //Flush Encoder
  2. ret = flush_encoder(ifmt_ctx,ofmt_ctx,0,framecnt);
  3. if (ret < 0) {
  4. printf("Flushing encoder failed\n");
  5. return -1;
  6. }
  7. //Write file trailer
  8. av_write_trailer(ofmt_ctx);
  9. //Clean
  10. if (video_st)
  11. avcodec_close(video_st->codec);
  12. av_free(out_buffer);
  13. avio_close(ofmt_ctx->pb);
  14. avformat_free_context(ifmt_ctx);
  15. avformat_free_context(ofmt_ctx);

flush_encoder的内容如下

  1. int flush_encoder(AVFormatContext *ifmt_ctx, AVFormatContext *ofmt_ctx, unsigned int stream_index, int framecnt){
  2. int ret;
  3. int got_frame;
  4. AVPacket enc_pkt;
  5. if (!(ofmt_ctx->streams[stream_index]->codec->codec->capabilities &
  6. CODEC_CAP_DELAY))
  7. return 0;
  8. while (1) {
  9. enc_pkt.data = NULL;
  10. enc_pkt.size = 0;
  11. av_init_packet(&enc_pkt);
  12. ret = avcodec_encode_video2 (ofmt_ctx->streams[stream_index]->codec, &enc_pkt,
  13. NULL, &got_frame);
  14. av_frame_free(NULL);
  15. if (ret < 0)
  16. break;
  17. if (!got_frame){
  18. ret=0;
  19. break;
  20. }
  21. printf("Flush Encoder: Succeed to encode 1 frame!\tsize:%5d\n",enc_pkt.size);
  22. //Write PTS
  23. AVRational time_base = ofmt_ctx->streams[stream_index]->time_base;//{ 1, 1000 };
  24. AVRational r_framerate1 = ifmt_ctx->streams[stream_index]->r_frame_rate;// { 50, 2 };
  25. AVRational time_base_q = { 1, AV_TIME_BASE };
  26. //Duration between 2 frames (us)
  27. int64_t calc_duration = (double)(AV_TIME_BASE)*(1 / av_q2d(r_framerate1));  //内部时间戳
  28. //Parameters
  29. enc_pkt.pts = av_rescale_q(framecnt*calc_duration, time_base_q, time_base);
  30. enc_pkt.dts = enc_pkt.pts;
  31. enc_pkt.duration = av_rescale_q(calc_duration, time_base_q, time_base);
  32. /* copy packet*/
  33. //转换PTS/DTS(Convert PTS/DTS)
  34. enc_pkt.pos = -1;
  35. framecnt++;
  36. ofmt_ctx->duration=enc_pkt.duration * framecnt;
  37. /* mux encoded frame */
  38. ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);
  39. if (ret < 0)
  40. break;
  41. }
  42. return ret;
  43. }

可以看到基本上就是把编码流程重复了一遍

至此,就实现了摄像头数据的直播。

当然还可以使用多线程来实现“按下回车键停止播放”这样的控制功能。

本工程源代码

转自:http://blog.csdn.net/nonmarking/article/details/48022387

[转载] ffmpeg超详细综合教程——摄像头直播的更多相关文章

  1. ffmpeg超详细综合教程——摄像头直播

    本文的示例将实现:读取PC摄像头视频数据并以RTMP协议发送为直播流.示例包含了1.ffmpeg的libavdevice的使用2.视频解码.编码.推流的基本流程具有较强的综合性.要使用libavdev ...

  2. VMware虚拟机下安装CentOS7.0超详细图文教程

    1.本文说明: 官方的第一个文本档案.也就是0_README.txt,大概意思是这样(渣翻译,但是大概意思还是有的). CentOS-7.0-1406-x86_64-DVD.iso:这个镜像(DVD ...

  3. 【公众号转载】超详细 Nginx 极简教程,傻瓜一看也会!

    什么是Nginx? Nginx (engine x) 是一款轻量级的Web 服务器 .反向代理服务器及电子邮件(IMAP/POP3)代理服务器. 什么是反向代理? 反向代理(Reverse Proxy ...

  4. MySql5.6 Window超详细安装教程

    林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 目录 一.安装包准备二.开始安装三.验证安装四.客户端工具 一.安装包准备 1.下载MySql ...

  5. MySql5.6Window超详细安装教程(msi 格式的安装)

    转自:红黑联盟  http://www.2cto.com/database/201506/409821.html 一.安装包准备 1.下载MySql5.6 http://www.mysql.com/ ...

  6. 在Ubuntu下进行XMR Monero(门罗币)挖矿的超详细图文教程

    大家都知道,最近挖矿什么的非常流行,于是我也在网上看了一些大神写的教程,以及跟一些大神请教过如何挖矿,但是网上的教程都感觉写得不够详细,于是今天我这里整理一个教程,希望能够帮到想要挖矿的朋友. 首先, ...

  7. 超详细实战教程丨多场景解析如何迁移Rancher Server

    本文转自Rancher Labs 作者介绍 王海龙,Rancher中国社区技术经理,负责Rancher中国技术社区的维护和运营.拥有6年的云计算领域经验,经历了OpenStack到Kubernetes ...

  8. 【建议收藏】Redis超详细入门教程大杂烩

    写在前边 Redis入门的整合篇.本篇也算是把2021年redis留下来的坑填上去,重新整合了一翻,点击这里,回顾我的2020与2021~一名大二后台练习生 NoSQL NoSQL(NoSQL = N ...

  9. 最新MATLAB R2021b超详细安装教程(附完整安装文件)

    摘要:本文详细介绍Matlab R2021b的安装步骤,为方便安装这里提供了完整安装文件的百度网盘下载链接供大家使用.从文件下载到证书安装本文都给出了每个步骤的截图,按照图示进行即可轻松完成安装使用. ...

随机推荐

  1. iOS后台如何保持socket长连接和数据传输

    工程中使用tcp长连接来和服务端进行数据传输,在IOS平台上,由于苹果的后台机制,会有以下问题: 当程序退到后台的时候,所有线程被挂起,系统会回收所有的socket资源,那么socket连接就会被关闭 ...

  2. hdu 4722 Good Numbers 数位DP

    数位DP!!! 代码如下: #include<iostream> #include<stdio.h> #include<algorithm> #include< ...

  3. MongoDB的安装,配置与开机自启动

    关于简介不多说百度去吧少年.. MongoDB详细安装: 1.进入官网,点击DOWNLOAD MONGODB,下载所需要的版本.. 我这里把下载的文件放在d\MongoDB文件夹下,点击下载的官方镜像 ...

  4. lintcode:买卖股票的最佳时机 III

    买卖股票的最佳时机 III 假设你有一个数组,它的第i个元素是一支给定的股票在第i天的价格.设计一个算法来找到最大的利润.你最多可以完成两笔交易. 样例 给出一个样例数组 [4,4,6,1,1,4,2 ...

  5. libevent简单介绍

    http://blog.csdn.net/mafuli007/article/details/7476014 1      简介 主页:http://www.monkey.org/~provos/li ...

  6. Keil 4.7a版本问题&Jlink Clone问题

    听PP说Keil 4.7A新出,支持代码自动补全.激动之至,keil官网急填,下载安装.  问题即刻遇见①,电脑蓝屏,安装包损坏.当下载安装包未下载完时,续传安装包没用了.还是重下载吧,免得浪费时间. ...

  7. 用PostGreSQL实现三层(复习)

    modal DAL,BLL都是类库的形式 最终结果如下: 数据库代码: -- Table: student -- DROP TABLE student; CREATE TABLE student ( ...

  8. iOS Architecture

    目前ios的指令集有以下几种: armv6 iPhone iPhone2 iPhone3G 第一代和第二代iPod Touch armv7 iPhone4 iPhone4S armv7s iPhone ...

  9. Netty4.x中文教程系列(三) Hello World !详解

    Netty 中文教程 (二) Hello World !详解 上一篇文章,笔者提供了一个Hello World 的Netty示例. 时间过去了这么久,准备解释一下示例代码. 1.HelloServer ...

  10. Data Flow ->> CDC Control Task, CDC Source, CDC Splitter

    CDC Control Task可以从控制CDC数据同步,比如初始化加载.LSN范围的管理.它可以代替另一种做法,就是通过调用一批CDC函数来完成同样的事情.从SSIS的角度来完成,事情编程简单,和另 ...