音视频新手项目-B站本地电影直播源码和文档分享

1 项目简介和效果展示

B站程序员老廖:音视频新手项目-B站本地电影直播源码和文档分享哔哩哔哩bilibili

1.1 项目简介

项目需求:在B站/微信视频号等平台以直播的方式循环播放一个本地视频。。

开发环境:FFmpeg6.0  + QT5.15.2 (QT只是作为IDE工具使用,不涉及QT UI界面)

原始的项目路径:https://github.com/juniorxsound/libav-RTMP-Streaming.git

注:我这里讲解的项目源码是老廖已经修改过的,GitHub上原有的代码在ffmpeg6.0会报错。

1.2 效果展示

项目效果演示:可以推到自己的流媒体服务器,也可以推送到B站直播间

1.2.1 推送到自己的流媒体服务器

srs流媒体服务器搭建教程:Build | SRS (ossrs.net)

推送到自己流媒体和拉流效果(左边是推流窗口,右边是拉流窗口)

1.2.2 推送到B站直播间

2 项目框图

难点:

  1. 能自己画出框架图(不少同学在学技术的时候没法先把项目的框架图整理出来);

  2. 如何按正常播放速度推送本地文件;

  3. 解复用 ->复用的时间转换。

后续可以考虑的扩展:

  1. 支持任意格式文件的推流(目前这个程序需要本地文件的视频/音频编码格式符合FLV的要求),如果需要支持任意格式,需要考虑对音频/视频进行解码后再编码;

  2. 添加文字水印或者图片水印,此时就必须 重新编码。

3 项目源码剖析

我们先分析整体的源码逻辑,然后再剖析具体函数的实现

3.1 源码框架图

Streamer: C++类,封装本地文件以RTMP协议推送到流媒体服务器

3.2 main函数入口

main函数要传入本地文件路径以及rtmp推流地址,比如用QT运行该程序时:

完整的main函数如下所示:

int main(int argc, char *argv[])
{
   int  ret = 0;
   std::cout << std::endl
       << "Welcome to RTMP streamer  " << std::endl
       << "Written by @juniorxsound <https://orfleisher.com>" << std::endl
       << std::endl;
   //检测参数输入情况
   if(argc != 3)
   {
       printf("please input video_file_name rtmp_server_address\n");
   }

   // 获取本地文件路径
   char *video_file_name = argv[1];
   // 获取rtmp推流地址
   char *rtmp_server_address = argv[2];
   // 构造一个推流实例
   Streamer streamer(video_file_name, rtmp_server_address);

   //    Streamer streamer("F://0voice//linux_ke//35-demux-decode//time.flv", "rtmp://114.215.169.66/live/darren");
   //初始化 解析本地文件媒体信息  创建输出流信息
   if(streamer.Init() < 0)
   {
       printf("Streamer Init failed\n");
       return -1;
   }
   //开始推流 和流媒体服务器交互 读取本地文件的包 按正常播放速度控制读取进度 按推流要求转换时间基 发送给流媒体服务器
   ret = streamer.Stream();

   #if 0
   //如果需要循环播放,可以按照以下流程进行
   int loop_count = 0;
   while(true) {
       printf("loop cout -> %d\n", ++loop_count); //打印循环播放次数
       ret = streamer.Init();
       if(ret < 0)
       {
           printf("Streamer Init failed\n");
           break;
       }
       ret = streamer.Stream();
       if(ret < 0) {
           printf("Streamer Stream failed\n");
           break;
       }
   }
   #endif
   return ret;
}

3.3 Streamer::Init()初始化函数

该函数主要 解析本地文件和创建输出流信息。

int Streamer::Init()
{
   int ret = 0;
   // 初始化网络 , 默认状态下, FFmpeg是不允许联网的 , 必须调用该函数 , 初始化网络后FFmpeg才能进行联网
   ret = avformat_network_init();
   if(ret < 0)
   {
       printf("avformat_network_init failed\n");
       return ret;
   }
   if(!video_file_name_)
   {                 //检测本地文件名是否为空
       printf("video_file_name is null\n");
       return -1;
   }
   if(!rtmp_server_address_)
   {             //检测rtmp地址是否为空
       printf("rtmp_server_address is null\n");
       return -1;
   }
   //解析本地文件媒体信息 并获取视频流index
   ret = setupInput(video_file_name_);     //打开本地文件
   if (ret < 0)
   {
       printf("setupInput failed\n");
       return ret;
   }
   // 创建输出流信息  对应的编码信息来自于     本地文件媒体信息
   ret = setupOutput(rtmp_server_address_);    //创建输出信息
   if (ret < 0)
   {
       printf("setupOutput failed\n");
       return ret;
   }
   return 0;
}

3.3.1  Streamer::setupInput()解析本地文件函数

这里之所以获取video index,目的是用来判断读取本地文件的packet是否为视频包,需要根据视频包的时间戳控制推流速度。

int Streamer::setupInput(const char *video_file_name)
{
   int ret = 0;
   // 解析本地文件媒体信息
   ret = avformat_open_input(&ifmt_ctx_, video_file_name, NULL, NULL);
   if (ret < 0)
   {
       printf("Could not open input file.");
       return -1;
   }
   ret = avformat_find_stream_info(ifmt_ctx_, NULL);
   if (ret < 0)
   {
       printf("Failed to retrieve input stream information");
       return -1;
   }
   // 获取视频流video_index
   for (uint32_t i = 0; i < ifmt_ctx_->nb_streams; i++)
   {
       if (ifmt_ctx_->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
       {
           video_index_ = i;  //找到视频索引的目的是为了在推流时按正常播放速率推送
           break;
       }
   }

   av_dump_format(ifmt_ctx_, 0, video_file_name, 0);
   return 0;
}

3.3.2 Streamer::setupOutput()创建输出流信息函数

int Streamer::setupOutput(const char *_rtmp_server_address)
{
   int ret = 0;
   ret = avformat_alloc_output_context2(&ofmt_ctx_, NULL, "flv", _rtmp_server_address); //RTMP
   if (ret < 0)
   {
       printf("Could not create output context\n");
       return -1;
   }

   ofmt_ = (AVOutputFormat *)ofmt_ctx_->oformat;
   for (uint32_t i = 0; i < ifmt_ctx_->nb_streams; i++)
   {
       //Create output AVStream according to input AVStream
       AVStream *in_stream = ifmt_ctx_->streams[i];
       // 创建输出流信息
       AVStream *out_stream = avformat_new_stream(ofmt_ctx_, NULL);
       if (!out_stream)
       {
           printf("Failed allocating output stream\n");
           return -1;
       }
       //对应的编码信息来自于本地文件媒体信息 Copy the settings of AVCodecParameters
       ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
       if (ret < 0)
       {
           printf("Failed to copy parameters from input to output stream codec parameters\n");
           return -1;
       }
       out_stream->codecpar->codec_tag = 0;
   }
   av_dump_format(ofmt_ctx_, 0, _rtmp_server_address, 1);
   return 0;
}

3.4 Streamer::Stream()推流函数

这里重点在于时间基的转换,需要将输入流的时间基转成输出流的时间基:

in_stream = ifmt_ctx_->streams[pkt_->stream_index];
       out_stream = ofmt_ctx_->streams[pkt_->stream_index];
       pkt_->pts = av_rescale_q_rnd(pkt_->pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
       pkt_->dts = av_rescale_q_rnd(pkt_->dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
       pkt_->duration = av_rescale_q(pkt_->duration, in_stream->time_base, out_stream->time_base);

难点在于推流速度的控制,需要以正常播放速度控制推流速度

//控制读取进度
if (pkt_->stream_index == video_index_)
{
   AVRational time_base = ifmt_ctx_->streams[video_index_]->time_base; //获取本地文件视频流的时间基
   AVRational time_base_q = {1, AV_TIME_BASE};
   // 这里存在bug的可能,因为不是所有文件的音视频流都是从0开始的,更正确的做法是要减去 ifmt_ctx_->start_time,但要判断其是不是AV_NOPTS_VALUE
   // 但不做处理大部分的视频文件也是没有问题的
   int64_t pts_time = av_rescale_q(pkt_->dts, time_base, time_base_q); //将dts转成微妙(us)的单位
   int64_t now_time = av_gettime() - start_time_;      //计算 开始推流时间 到当前时间 经过的时间
   if (pts_time > now_time)                        //如果要推送的 数据 快于 正常播放时间 则休眠
       av_usleep(pts_time - now_time);
   if(now_time - print_last_time > print_interval) {
       printf("play time: %0.3fs\n", now_time/1000000.0); //转成秒的单位
       print_last_time = now_time;
   }
}

完整的Stream()函数代码:

int Streamer::Stream()
{
   int ret = 0;
   //Open output URL
   if (!(ofmt_->flags & AVFMT_NOFILE))
   {
       ret = avio_open(&ofmt_ctx_->pb, rtmp_server_address_, AVIO_FLAG_WRITE);
       if (ret < 0)
       {
           printf("Could not open output URL '%s'", rtmp_server_address_);
           return -1;
       }
   }
   //和流媒体服务器交互 Write file header
   ret = avformat_write_header(ofmt_ctx_, NULL);
   if (ret < 0)
   {
       printf("Error occurred when opening output URL\n");
       return -1;
   }

   pkt_ = av_packet_alloc();
   start_time_ = av_gettime();       //这个单位是us
   int64_t print_interval = 2*1000*1000;   //单位是us,2*1000*1000us = 2*1000ms = 2s
   int64_t print_last_time = 0;
   while (1)
   {
       AVStream *in_stream, *out_stream;
       //读取本地文件的包
       ret = av_read_frame(ifmt_ctx_, pkt_);
       if (ret < 0) {
           printf("av_read_frame have error or end of file\n");
           break;
       }
       if (pkt_->pts == AV_NOPTS_VALUE)
       {
           //Write PTS
           AVRational time_base1 = ifmt_ctx_->streams[video_index_]->time_base;
           //Duration between 2 frames (us)
           int64_t calc_duration = (double)AV_TIME_BASE / av_q2d(ifmt_ctx_->streams[video_index_]->r_frame_rate);
           //Parameters
           pkt_->pts = (double)(frame_index_ * calc_duration) / (double)(av_q2d(time_base1) * AV_TIME_BASE);
           pkt_->dts = pkt_->pts;
           pkt_->duration = (double)calc_duration / (double)(av_q2d(time_base1) * AV_TIME_BASE);
       }
       //控制读取进度
       if (pkt_->stream_index == video_index_)
       {
           AVRational time_base = ifmt_ctx_->streams[video_index_]->time_base;
           AVRational time_base_q = {1, AV_TIME_BASE};
           int64_t pts_time = av_rescale_q(pkt_->dts, time_base, time_base_q);
           int64_t now_time = av_gettime() - start_time_;
           if (pts_time > now_time)
               av_usleep(pts_time - now_time);
           if(now_time - print_last_time > print_interval) {
               printf("play time: %0.3fs\n", now_time/1000000.0); //转成秒的单位
               print_last_time = now_time;
           }
       }

       //按推流要求转换时间基
       in_stream = ifmt_ctx_->streams[pkt_->stream_index];
       out_stream = ofmt_ctx_->streams[pkt_->stream_index];
       pkt_->pts = av_rescale_q_rnd(pkt_->pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
       pkt_->dts = av_rescale_q_rnd(pkt_->dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
       pkt_->duration = av_rescale_q(pkt_->duration, in_stream->time_base, out_stream->time_base);
       pkt_->pos = -1;
       if (pkt_->stream_index == video_index_)
       {
           frame_index_++;
       }
       ret = av_write_frame(ofmt_ctx_, pkt_);
//        ret = av_interleaved_write_frame(ofmt_ctx_, pkt_);

       if (ret < 0)
       {
           printf("Error muxing packet\n");
           break;
       }
   }

   //Write file trailer
   av_write_trailer(ofmt_ctx_);
   avformat_close_input(&ifmt_ctx_);
   if (ofmt_ctx_ && !(ofmt_->flags & AVFMT_NOFILE))
       avio_close(ofmt_ctx_->pb);
   av_packet_free(&pkt_);
   avformat_free_context(ofmt_ctx_);
   if (ret < 0 && ret != AVERROR_EOF)
   {
       printf("Error occurred.\n");
       return -1;
   }
   return 0;
}

4 项目展望

该项目的目的是掌握解复用/复用/时间基转换,以及如何控制推流速度,但这个项目还不足以支撑找工作,切记。

更多音视频的知识分享,可以持续关注程序员老廖。

PS: 后续可以考虑增加文字水印或者图片水印,这样你的直播影院更有标识度。

资源下载: