前言

使用ffmpeg推流很简单,使用ffmpeg命令推流更简单。本篇以本文标题《ffmpeg推流flv到rtmp》为中心。只推流输入flv格式的媒体文件,只推流到rtmp。

原因很简单,简化一切复杂的流程,稍后再说原因。我们通过多篇慢慢的提升代码复杂度,例如:把mp4推流到rtmp需要使用ffmpeg代码做什么和flv推流到rtmp有什么不同,等问题都会讲解。

编写代码

为了方便我引入的头文件有点多:

#ifndef __FFMPEG_COMMON_H
 #define __FFMPEG_COMMON_H
 
 extern "C" {
 #include <libavutil/frame.h>
 #include <libavutil/imgutils.h>
 #include <libavutil/time.h>
 #include <libswscale/swscale.h>
 #include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include "libavdevice/avdevice.h"
#include "libswresample/swresample.h"
#include <libavutil/dict.h>
#include <libavutil/samplefmt.h>
#include <libavutil/timestamp.h>
}

#endif

技巧:

你可以专门提出来一个.h头文件,引入一些公共的头文件,这样再写一个新.cpp文件的时候就不用每次都引入这么多了。

ffmpeg推流flv到rtmp_音视频编解码

这样其他地方只很简单的引入即可:

ffmpeg推流flv到rtmp_音视频开发_02

整体函数模型:

/**
 * 只能推流RTMP数据
 * @param input_filename 输入文件
 * @param output_filename 输出流地址
 * @return
 */
int ff_push_flv_to_rtmp_stream(char *input_filename, char *output_filename){
    ...
}

创建输入和输出的ffmpeg上下文:

int ret = 0;

// in stream
AVFormatContext *ctx = NULL;

// out stream
AVFormatContext *octx = NULL;

打开输入文件,初始化输入上下文

//    1.打开输入文件,返回 AVFormatContext
  ret = avformat_open_input(&ctx, input_filename, NULL, NULL);
  if(ret < 0){
      AV_PRT("open inputfile=%s error\n", input_filename);
      return -1;
  }

查找分析输入媒体流

//    2.查找流
ret = avformat_find_stream_info(ctx, NULL);
if(ret < 0) {
    AV_PRT("avformat_find_stream_info=%s error\n", input_filename);
    return -1;
}

创建输出流上下文

//创建输出流上下文
ret = avformat_alloc_output_context2(&octx, 0, "flv",output_filename);
if (!octx) {
    AV_PRT("Couldn't avformat_alloc_output_context2 %d.\n", ret);
    return 0;
}

创建输出流

因为输入的是flv,所以直接把输入的参数都拷贝过来了。
需要注意的是:如果是mp4什么的, 就需要其他的解码,编码处理了。

// 创建输出流
 for (int i = 0; i < ctx->nb_streams; ++i) {
     AVStream *s = ctx->streams[i];
     const AVCodec * codec= avcodec_find_decoder(s->codecpar->codec_id);
     AVStream *out = avformat_new_stream(octx, codec);
 
     //拷贝相关编码器信息
     avcodec_parameters_copy(out->codecpar, s->codecpar);
     //        out->codecpar->codec_tag = 0;
}

dump信息:

这里也需要注意,为什么两个av_dump_format之间需要增加休眠的sleep代码?这个可以自己做个实验把sleep去掉试试。欢迎留言实验结果。

AV_PRT("输入信息: \n");
av_dump_format(ctx, 0 , input_filename, 0);
av_usleep(1 * 1000 * 1000);
AV_PRT("输出信息: \n");
av_dump_format(octx, 0 , output_filename, 1);

打开输出流

//打开输出io流
ret = avio_open2(&octx->pb, output_filename, AVIO_FLAG_WRITE,&octx->interrupt_callback,NULL);
if (!octx->pb) {
    AV_PRT("Couldn't avio_open outUrl: %s ,ret =  %d.\n",output_filename, ret);
    avformat_free_context(octx);
    octx = NULL;
    return 0;
}
av_opt_set(octx->priv_data, "pes_payload_size", "0", 0);

写入头数据

//写入头信息
 ret = avformat_write_header(octx, 0);
 if (ret < 0) {
     AV_PRT("Couldn't avformat_write_header,ret = %d.\n", ret);
     if (octx && !(octx->oformat->flags & AVFMT_NOFILE)) {
         avio_close(octx->pb);
         octx->pb = NULL;
     }
     avformat_free_context(octx);
    octx = NULL;
    return 0;
}

创建AVPacket,读输入数据时用到,因为ffmpeg会把读取到的数据,封装成一个一个的AVPacket

AVPacket *avPacket = NULL;
avPacket = av_packet_alloc();

获取当前时间

int64_t start_time = av_gettime();

循环读取数据

while (av_read_frame(ctx, avPacket) >= 0)
 {
     //计算转换pts dts
     AVRational i_tb = ctx->streams[avPacket->stream_index]->time_base;
     AVRational o_tb = octx->streams[avPacket->stream_index]->time_base;
     avPacket->pts = av_rescale_q_rnd(avPacket->pts, i_tb, o_tb, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_NEAR_INF));
     avPacket->dts = av_rescale_q_rnd(avPacket->pts, i_tb, o_tb, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_NEAR_INF));
     avPacket->duration = av_rescale_q_rnd(avPacket->duration, i_tb, o_tb, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_NEAR_INF));
     avPacket->pos = -1;

    //视频帧推送速度
    if (ctx->streams[avPacket->stream_index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
    {
        AVRational tb = ctx->streams[avPacket->stream_index]->time_base;
        //已经过去的时间
        int64_t now = av_gettime() - start_time;
        int64_t dts = avPacket->dts * (1000 * 1000 * r2d(tb));
        if (dts > now)
            av_usleep(dts - now);
    }

    //写入流数据
    ret = av_interleaved_write_frame(octx, avPacket);
    if (ret<0)
    {
        break;
    }

    //unref AVPacket
    av_packet_unref(avPacket);
    if(ret < 0)
        break;
}

释放资源

//exit
 av_write_trailer(octx);
 if (octx && !(octx->oformat->flags & AVFMT_NOFILE)) {
     avio_close(octx->pb);
     octx->pb = NULL;
 }
 avformat_free_context(octx);
 
 if(!ctx){
    avformat_close_input(&ctx);
}
if(!avPacket){
    av_packet_free(&avPacket);
}

main函数:

int main (int argc, char **argv)
 {
     //    char *default_filename = "/home/zhenghui/视频/1080P.flv";
     char *default_filename = "/home/zhenghui/视频/cctv1.flv";
     char *output_filename = "rtmp://192.168.1.106/live/av?secret=3573a0c4b84e4d288524da305ab40b00";
 
     char *input_filename = NULL;
     int ret = 0;
     if(argc == 2){
        input_filename = argv[1];
    }else{
        input_filename = default_filename;
    }

    AV_PRT("input:%s \n", input_filename);
    AV_PRT("output:%s \n", output_filename);

    ff_push_flv_to_rtmp_stream(input_filename, output_filename);
    return 0;
}

测试

运行程序:

ffmpeg推流flv到rtmp_音视频开发_03

ffplay播放:

ffmpeg推流flv到rtmp_rtmp_04

ffmpeg推流flv到rtmp_音视频开发_05

我们输入一个mp4测试下:

ffmpeg推流flv到rtmp_rtmp_06

[flv @ 0x5fe24c7bcc80] Tag avc1 incompatible with output codec id '27' ([7][0][0][0])

因为我们只实现了最最最简单的把flv推流到了rtmp,没有加入编码和解码的功能部分,所以不行。

下一篇,我们就来研究下,如何把mp4和flv等格式兼容起来。

总结

因为rtmp只能接收flv格式的视频容器,所以需要把数据封装成flv之后,rtmp才可以识别到。