zoukankan      html  css  js  c++  java
  • ffmpeg综合应用示例(一)——摄像头直播

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

    1、ffmpeg的libavdevice的使用

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

    具有较强的综合性。

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

    [cpp] view plain copy
     
    1. avdevice_register_all();  


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

    [cpp] view plain copy
     
    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============= ");  
    7.     avformat_open_input(&pFmtCtx, "video=dummy", iformat, &options);  
    8.     printf("======================== ");  


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

    [cpp] view plain copy
     
    1. ffmpeg -list_devices true -f dshow -i dummy   


    以上语句得到的结果如下

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

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

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

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

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

    [cpp] view plain copy
     
    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! (没有找到合适的编码器!) ");  
    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.   
    21.     //H264 codec param  
    22.     //pCodecCtx->me_range = 16;  
    23.     //pCodecCtx->max_qdiff = 4;  
    24.     //pCodecCtx->qcompress = 0.6;  
    25.     pCodecCtx->qmin = 10;  
    26.     pCodecCtx->qmax = 51;  
    27.     //Optional Param  
    28.     pCodecCtx->max_b_frames = 3;  
    29.     // Set H264 preset and tune  
    30.     AVDictionary *param = 0;  
    31.     av_dict_set(&param, "preset", "fast", 0);  
    32.     av_dict_set(&param, "tune", "zerolatency", 0);  
    33.   
    34.     if (avcodec_open2(pCodecCtx, pCodec,&param) < 0){  
    35.         printf("Failed to open encoder! (编码器打开失败!) ");  
    36.         return -1;  
    37.     }  
    38.   
    39.     //Add a new stream to output,should be called by the user before avformat_write_header() for muxing  
    40.     video_st = avformat_new_stream(ofmt_ctx, pCodec);  
    41.     if (video_st == NULL){  
    42.         return -1;  
    43.     }  
    44.     video_st->time_base.num = 1;  
    45.     video_st->time_base.den = 25;  
    46.     video_st->codec = pCodecCtx;  
    47.   
    48.     //Open output URL,set before avformat_write_header() for muxing  
    49.     if (avio_open(&ofmt_ctx->pb,out_path, AVIO_FLAG_READ_WRITE) < 0){  
    50.     printf("Failed to open output file! (输出文件打开失败!) ");  
    51.     return -1;  
    52.     }  
    53.   
    54.     //Show some Information  
    55.     av_dump_format(ofmt_ctx, 0, out_path, 1);  
    56.   
    57.     //Write File Header  
    58.     avformat_write_header(ofmt_ctx,NULL);  


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

    [cpp] view plain copy
     
    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);  


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

    [cpp] view plain copy
     
    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 ");  
    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 ");  
    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.   
    24.             enc_pkt.data = NULL;  
    25.             enc_pkt.size = 0;  
    26.             av_init_packet(&enc_pkt);  
    27.             ret = avcodec_encode_video2(pCodecCtx, &enc_pkt, pFrameYUV, &enc_got_frame);  
    28.             av_frame_free(&pframe);  
    29.             if (enc_got_frame == 1){  
    30.                 //printf("Succeed to encode frame: %5d size:%5d ", framecnt, enc_pkt.size);  
    31.                 framecnt++;   
    32.                 enc_pkt.stream_index = video_st->index;  
    33.   
    34.                 //Write PTS  
    35.                 AVRational time_base = ofmt_ctx->streams[videoindex]->time_base;//{ 1, 1000 };  
    36.                 AVRational r_framerate1 = ifmt_ctx->streams[videoindex]->r_frame_rate;// { 50, 2 };  
    37.                 AVRational time_base_q = { 1, AV_TIME_BASE };  
    38.                 //Duration between 2 frames (us)  
    39.                 int64_t calc_duration = (double)(AV_TIME_BASE)*(1 / av_q2d(r_framerate1));  //内部时间戳  
    40.                 //Parameters  
    41.                 //enc_pkt.pts = (double)(framecnt*calc_duration)*(double)(av_q2d(time_base_q)) / (double)(av_q2d(time_base));  
    42.                 enc_pkt.pts = av_rescale_q(framecnt*calc_duration, time_base_q, time_base);  
    43.                 enc_pkt.dts = enc_pkt.pts;  
    44.                 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));  
    45.                 enc_pkt.pos = -1;  
    46.                   
    47.                 //Delay  
    48.                 int64_t pts_time = av_rescale_q(enc_pkt.dts, time_base, time_base_q);  
    49.                 int64_t now_time = av_gettime() - start_time;  
    50.                 if (pts_time > now_time)  
    51.                     av_usleep(pts_time - now_time);  
    52.   
    53.                 ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);  
    54.                 av_free_packet(&enc_pkt);  
    55.             }  
    56.         }  
    57.         else {  
    58.             av_frame_free(&pframe);  
    59.         }  
    60.         av_free_packet(dec_pkt);  
    61.     }  

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

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

    [cpp] view plain copy
     
    1. #define         AV_TIME_BASE   1000000  


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

    [cpp] view plain copy
     
    1. timestamp=AV_TIME_BASE*time(s)  


    所以有

    [cpp] view plain copy
     
    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为时间基来表示的,时间基之间的转换用下式

    [cpp] view plain copy
     
    1. enc_pkt.pts = av_rescale_q(framecnt*calc_duration, time_base_q, time_base);  

    其实就是

    [cpp] view plain copy
     
    1. enc_pkt.pts = (double)(framecnt*calc_duration)*(double)(av_q2d(time_base_q)) / (double)(av_q2d(time_base));  

    非常简单的数学转换。

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

    [cpp] view plain copy
     
    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操作,输出之前存储在缓冲区内的数据

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


    flush_encoder的内容如下

    [cpp] view plain copy
     
    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! size:%5d ",enc_pkt.size);  
    22.   
    23.         //Write PTS  
    24.         AVRational time_base = ofmt_ctx->streams[stream_index]->time_base;//{ 1, 1000 };  
    25.         AVRational r_framerate1 = ifmt_ctx->streams[stream_index]->r_frame_rate;// { 50, 2 };  
    26.         AVRational time_base_q = { 1, AV_TIME_BASE };  
    27.         //Duration between 2 frames (us)  
    28.         int64_t calc_duration = (double)(AV_TIME_BASE)*(1 / av_q2d(r_framerate1));  //内部时间戳  
    29.         //Parameters  
    30.         enc_pkt.pts = av_rescale_q(framecnt*calc_duration, time_base_q, time_base);  
    31.         enc_pkt.dts = enc_pkt.pts;  
    32.         enc_pkt.duration = av_rescale_q(calc_duration, time_base_q, time_base);  
    33.   
    34.         /* copy packet*/  
    35.         //转换PTS/DTS(Convert PTS/DTS)  
    36.         enc_pkt.pos = -1;  
    37.         framecnt++;  
    38.         ofmt_ctx->duration=enc_pkt.duration * framecnt;  
    39.   
    40.         /* mux encoded frame */  
    41.         ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);  
    42.         if (ret < 0)  
    43.             break;  
    44.     }  
    45.     return ret;  
    46. }  

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

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

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

    本工程源代码

    from:https://blog.csdn.net/nonmarking/article/details/48022387

  • 相关阅读:
    用dt命令搜索查看符号
    烦人的异常
    _NT_SYMBOL_PROXY
    Windbg常用命令系列---.f+, .f- (切换Local Context)
    Windbg常用命令系列---.dumpcab (创建dump CAB文件)
    Windbg常用命令系列---.dump(创建dump文件)
    Windbg常用命令系列---!mapped_file
    Windbg常用命令系列---!cppexr
    再谈FPO
    Windbg常用命令系列---!stl
  • 原文地址:https://www.cnblogs.com/lidabo/p/8663043.html
Copyright © 2011-2022 走看看