ffmpeg综合应用示例(一)——摄像头直播

43 篇文章 0 订阅

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

1、ffmpeg的libavdevice的使用

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

具有较强的综合性。

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

 

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,这样与在命令行中输入下列命令有相同的效果

 

 

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.  
  6. //Show Dshow Device

  7. show_dshow_device();

  8.  
  9. printf("\nChoose capture device: ");

  10. if (gets(capture_name) == 0)

  11. {

  12. printf("Error in gets()\n");

  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.(无法打开输入流)\n");

  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.(无法获取流信息)\n");

  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.(没有找到视频流)\n");

  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.(无法打开解码器)\n");

  45. return -1;

  46. }

在选择了输入设备并进行相关初始化之后,需要对输出做相应的初始化。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.  
  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! (编码器打开失败!)\n");

  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! (输出文件打开失败!)\n");

  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格式,所以要先做如下的准备工作

 

 

 
  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.  
  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\tsize:%5d\n", 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,定义为

 

#define         AV_TIME_BASE   1000000


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

 

 

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为时间基来表示的,时间基之间的转换用下式

 

 

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

其实就是

 

 

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.  
  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的内容如下

 

 

 
  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.  
  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. }

 

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是使用FFmpeg.AutoGen库在C#中解码摄像头视频流的示例代码: ```csharp using System; using FFmpeg.AutoGen; namespace FFmpegDecodingExample { class Program { static void Main(string[] args) { // Initialize FFmpeg library ffmpeg.av_register_all(); // Open video device AVFormatContext* videoInputContext = null; if (ffmpeg.avformat_open_input(&videoInputContext, "video_device_path", null, null) != 0) { Console.WriteLine("Failed to open video device"); return; } // Find video stream if (ffmpeg.avformat_find_stream_info(videoInputContext, null) < 0) { Console.WriteLine("Failed to find stream information"); return; } int videoStreamIndex = -1; for (int i = 0; i < videoInputContext->nb_streams; i++) { if (videoInputContext->streams[i]->codec->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO) { videoStreamIndex = i; break; } } if (videoStreamIndex == -1) { Console.WriteLine("Failed to find video stream"); return; } AVCodecContext* videoCodecContext = videoInputContext->streams[videoStreamIndex]->codec; // Find video decoder AVCodec* videoCodec = ffmpeg.avcodec_find_decoder(videoCodecContext->codec_id); if (videoCodec == null) { Console.WriteLine("Failed to find video decoder"); return; } // Open video decoder if (ffmpeg.avcodec_open2(videoCodecContext, videoCodec, null) < 0) { Console.WriteLine("Failed to open video decoder"); return; } // Read video frames AVPacket* packet = ffmpeg.av_packet_alloc(); AVFrame* frame = ffmpeg.av_frame_alloc(); while (ffmpeg.av_read_frame(videoInputContext, packet) >= 0) { if (packet->stream_index == videoStreamIndex) { // Decode video packet int ret = ffmpeg.avcodec_send_packet(videoCodecContext, packet); if (ret < 0 || ret == ffmpeg.AVERROR(ffmpeg.EAGAIN) || ret == ffmpeg.AVERROR_EOF) { Console.WriteLine("Failed to send video packet for decoding"); break; } while (ret >= 0) { ret = ffmpeg.avcodec_receive_frame(videoCodecContext, frame); if (ret < 0 && ret != ffmpeg.AVERROR(ffmpeg.EAGAIN) && ret != ffmpeg.AVERROR_EOF) { Console.WriteLine("Failed to receive video frame after decoding"); break; } // Process and display video frame // Release frame resources ffmpeg.av_frame_unref(frame); } } // Release packet resources ffmpeg.av_packet_unref(packet); } // Release resources ffmpeg.avformat_close_input(&videoInputContext); ffmpeg.avcodec_close(videoCodecContext); ffmpeg.avformat_free_context(videoInputContext); ffmpeg.av_frame_free(&frame); ffmpeg.av_packet_free(&packet); } } } ``` 在示例代码中,您需要将"video_device_path"替换为实际的视频设备路径。示例代码打开视频设备并查找视频流。然后,找到视频解码器并打开解码器。最后,使用`avcodec_send_packet()`方法将视频数据包发送给解码器,并使用`avcodec_receive_frame()`方法接收解码后的帧。 请注意,这只是一个基本示例,实际使用时可能需要根据您的具体需求进行更多的参数设置和错误处理。还可以根据需要进行帧处理、显示或保存等操作。 希望这个示例能对您有所帮助!如果您有任何更具体的问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值