使用ffmpeg实现rtmp推流

服务器用的是ngix+rtmp,配置方式见博客:
https://blog.csdn.net/goldfish3/article/details/100036059

  1. 由于我是在本机配置的ngix服务器,所以host是127.0.0.1,如果用的虚拟机,就是虚拟机的ip地址。
  2. 路径中的rtmplive是ngix配置文件里指定的,room是自己写的路径。

代码涉及一些ffmpeg的知识点,这里列一下:

1: PTS 和 DTS

h264帧中,B帧需要前面的帧和后面的帧,才能解码,因此解码时间和播放时间会不一样,比如一段帧:
按播放顺序为:I B B P
但由于B帧解码需要P帧,因此解码顺序为:I P B B
因此,解码序列和播放序列就需要使用不同的时间戳来表示~
PTS:显示时间戳,对应的数据格式为 AVFrame(解码后的帧),通过 AVCodecContext->time_base 获取。
DTS:解码时间戳,对应的数据格式为AVPacket(解码前的包),通过AVStream->time_base获取。

2: AVRational

而ffmpeg中,时间戳并不是以自然时间表示的,而是以一个个格子的形式,不同的格子代表不同的 time base。格子本身使用结构体 AVRational表示:

typedef struct AVRational{
    int num; //分子
    int den; //分母
} AVRational;

比如num=1,den=200,一个格子就表示 1/200 秒,PTS和DTS中的时间戳都是用 AVRational表示的。

3:推流时间同步问题

我们应该在什么时间,将包推给客户端?下面的代码中选用的是包解码的时间,也就是DTS时间,因此,如果当前要推的包还没有到解码时间,就需要等一会儿再推。真实环境下,由于网络问题等,处理会更更复杂一些。

下面代码涉及的api

AVFormatContext():c语言不是面向对象的,因此所有参数和对象需要使用一个上下文进行传递和管理,ffmpeg里就是它了。
avformat_open_input():通过路径,将视频信息读入上下文。
avformat_find_stream_info():获取流信息。
av_dump_format():获取视频封装信息,比如视频中有哪些流,流的具体情况。
avformat_network_init():网络初始化
avformat_alloc_output_context2():新建输出上下文
avformat_new_stream():新建流,并将流放入上下文
avcodec_parameters_copy():拷贝流参数
avio_open():打开流,在这里输出流是网络,应该就是作网络推流的一些准备工作和参数设置
avformat_write_header():写入头信息(也不知掉这些头信息是从哪来的)
av_read_frame():虽然叫 read frame,但是这里实际上读取的是一个包(AVPacket),也就是解码前的数据。
av_q2d():将AVRational转化为秒
av_interleaved_write_frame():将数据排序,写入

总的流程大概就是:
获取输出上下文 -> 建立输出上下文 -> 复制流参数信息到输出上下文 -> 使用输出上下文打开输出流 -> 从输入流中读取数据,使用输出流推出去。

#include 
#include "string.h"

extern "C"{
#include 
#include 
#include 
#include 
#include "libavutil/time.h"
#include 
}
using namespace std;
//
#include 
#define __STDC_CONSTANT_MACROS
#define USE_H264BSF 0

void printErrWithCode(int errCode){
    char buf[1024] = {0};
    av_strerror(errCode, buf, sizeof(buf));
    cout << buf << endl;
    return;
}

int main(){
    AVFormatContext *ictx = NULL;
    AVFormatContext *octx = NULL;
    string in_path = "/Users/heyutang/Desktop/test.flv";
    string out_path = "rtmp://127.0.0.1:1935/rtmplive/room";
    int ret = 0;
    avformat_network_init();

    //打开文件,解封装协议头
    //类似于mp4都有文件头,文件头里放了所有的文件信息。
    ret = avformat_open_input(&ictx, in_path.c_str(), 0, 0);
    if (ret) {
        printErrWithCode(ret);
    }
    ret = avformat_find_stream_info(ictx, NULL);
    av_dump_format(ictx, 0, in_path.c_str(), false);

    //第二个参数是输出格式
    ret = avformat_alloc_output_context2(&octx, NULL, "flv", out_path.c_str());
    
    if (!octx) {
        printErrWithCode(ret);
    }

    //遍历所有输出流
    for (int i=0; i<ictx->nb_streams; i++) {
        //创建一个输出流
        AVStream *out = avformat_new_stream(octx, ictx->streams[i]->codec->codec);
        if (!out) {
            ret = 0;
            printErrWithCode(ret);
        }

        //将输入流的配置信息(AVCodecContext)复制到输出流中
        //上面这种是老的方式,下面这种是新的,但是对mp4文件可能会有问题
//        avcodec_copy_context(out->codec, ictx->streams[i]->codec);
        avcodec_parameters_copy(out->codecpar, ictx->streams[i]->codecpar);
        out->codec->codec_tag = 0;
    }
    av_dump_format(octx, 0, out_path.c_str(), true);

    //打开流,写入头信息,至此,输出上下文建立完成
    ret = avio_open(&octx->pb, out_path.c_str(), AVIO_FLAG_WRITE);
    if (!octx->pb) {
        printErrWithCode(ret);
    }
    ret = avformat_write_header(octx, 0);
    if (ret < 0) {
        printErrWithCode(ret);
    }

    //推流每一帧数据
    AVPacket pkt;
    long long startTime = av_gettime(); //获取当前的时间戳(微秒)
    while (1) {
        ret = av_read_frame(ictx, &pkt);    //这一步虽然名字叫 read_frame,但实际上读取出来的是 packet
        if (ret) {
            printErrWithCode(ret);
        }
        //计算转换 pts dts(time base可能不同)(但是实际上,段点调试发现time_base是相同的,不知道是哪一步复制的time_base)
//        AVRational itime = ictx->streams[pkt.stream_index]->time_base;
//        AVRational otime = octx->streams[pkt.stream_index]->time_base;
//        pkt.pts = av_rescale_q_rnd(pkt.pts, itime, otime,
//                    (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
//        pkt.dts = av_rescale_q_rnd(pkt.dts, itime,otime,
//                    (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
//        pkt.duration = av_rescale_q_rnd(pkt.duration, itime, otime,
//                    (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
//        pkt.pos = -1;

        //这里假设网络传输是没问题的,那么我们应该在dts也就是解码时间进行推流。
        //如果推流时间早于解码时间,那么就等一会儿。
        if (ictx->streams[pkt.stream_index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            AVRational tb = ictx->streams[pkt.stream_index]->time_base;
            long long now = av_gettime() - startTime;
            long long dts = pkt.dts * (1000 * 1000 * av_q2d(tb)); //将秒转化为微秒
            if (dts > now) {
                av_usleep(dts - now);
            }
        }
        //根据pts dts进行排序,然后写入(由于是网络流,这一段就是网络传输)
        ret = av_interleaved_write_frame(octx, &pkt);
        if (ret < 0) {
            printErrWithCode(ret);
        }
        av_packet_unref(&pkt);
    }
}

你可能感兴趣的:(音视频)