直播入门(四)通过代码实现一个简单的推流器

FFmpeg版本:4.1.3

原理

从前面的文章,我们可以知道,实现推流客户端需要执行的下面几个步骤:

  1. 采集
  2. 编码
  3. 封装
  4. 推流

本文实现的是将本地的文件推送到服务器的过程,因此,不存在采集和编码过程。只有封装和推流的过程。

代码分析

源代码在 :https://github.com/WaPonX/FFmpegDemo
源代码中用到的一些关键的FFmpeg函数解释可以看:直播入门(附录二)FFmpeg关键函数一览表

环境的配置和播放,我们在直播入门(三)动手实现一个简单的直播中讲过的,这里不再赘述了。

初始化

av_register_all() 这个函数在FFmpeg 4.0 中已经被标记为废弃了。因此不再需要该函数进行初始化。
另外也不用调用avformat_network_init()函数进行初始化了。

bool Pusher::Initialize()
{
    int ret = 0;

    /// 为输入分配一个AVFormatContext对象
    m_inputContext = avformat_alloc_context();
    if(nullptr == m_inputContext)
    {
        return false;
    }

    /// 打开输入流,注意调用了这个函数之后,m_inputContext才的iformat才会被分配对象
    /// 否则会被设置为空
    /// FFmpeg的说明中,也强调了AVFormatContext中的iformat应该由该函数来分配对象,不能手动赋值。
    ret = avformat_open_input(&m_inputContext, m_input.c_str(), nullptr, nullptr);
    if (ret < 0)
    {
        LOG("Could not open input file. error code is %d", ret);
        avformat_close_input(&m_inputContext);
        return false;
    }
    LOG("Input format %s, duration %lld us", m_inputContext->iformat->long_name, m_inputContext->duration);

    /// 从上下文中解析流数据
    ret = avformat_find_stream_info(m_inputContext, nullptr);
    if (ret < 0)
    {
        LOG("Failed to retrieve input stream information. error code is %d", ret);
        avformat_close_input(&m_inputContext);
    }
    av_dump_format(m_inputContext, 0, m_input.c_str(), 0);

    /// 为输出流分配一个上下文对象,指定输出流的格式
    avformat_alloc_output_context2(&m_outputContext, nullptr, "flv", m_output.c_str());
    LOG("Input format %s, duration %lld us", m_outputContext->oformat->long_name, m_outputContext->duration);

    if (m_outputContext == nullptr)
    {
        LOG("Could not create output context\n");
        CloseContext(m_inputContext, m_outputContext);
        return false;
    }

    /// 从输入流中复制AVStream对象。
    for (uint32_t index = 0; index < m_inputContext->nb_streams; ++index)
    {
        //根据输入流创建输出流
        AVStream *inStream = m_inputContext->streams[index];
        AVStream *outStream = avformat_new_stream(m_outputContext, inStream->codec->codec);
        if (nullptr == outStream)
        {
            LOG("Failed allocating output/input stream\n");
            CloseContext(m_inputContext, m_outputContext);
            return false;
        }
        //复制AVCodecContext的设置
        ret = avcodec_copy_context(outStream->codec, inStream->codec);
        if (ret < 0)
        {
            LOG("Failed to copy context from input to output stream codec context\n");
            CloseContext(m_inputContext, m_outputContext);
            return false;
        }
        outStream->codec->codec_tag = 0;
        if (m_outputContext->oformat->flags & AV_CODEC_FLAG_GLOBAL_HEADER)
        {
            outStream->codec->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
        }
    }

    av_dump_format(m_outputContext, 0, m_output.c_str(), 1);
    /// 打开输出端,建立连接,访问URL。
    if (!(m_outputContext->oformat->flags & AVFMT_NOFILE))
    {
        /// 使用写标记打开
        int ret = avio_open(&m_outputContext->pb, m_output.c_str(), AVIO_FLAG_WRITE);
        if (ret < 0)
        {
            LOG("Could not open output URL '%s', Error Code is %d", m_output, ret);
            CloseContext(m_inputContext, m_outputContext);
            return false;
        }
    }

    /// 记录视频流和音频流的StreamID
    ret = ParseVideoAndAudioStreamIndex();
    if (ret != 0)
    {
        return false;
    }

    m_isInit = true;
    return true;
}

初始化的操作,主要就是从输入流中创建输出流,并且记录一些信息。

推送数据

推送的主要实现参考的是雷神的代码:最简单的基于FFmpeg的推流器(以推送RTMP为例)

int32_t Pusher::Push()
{
    //写文件头
    int ret = avformat_write_header(m_outputContext, NULL);

    if (ret < 0)
    {
        LOG("Error occurred when opening output URL, Error Code is %d\n", ret);
        CloseContext(m_inputContext, m_outputContext);
    }
    AVPacket packet;
    uint32_t videoWriteFrameCount = 0;
    int64_t start_time = av_gettime();
    while (true)
    {
        AVStream *inStream, *outStream;
        //获取一个数据包
        ret = av_read_frame(m_inputContext, &packet);
        if (ret < 0)
        {
            LOG("faild to read one packet from input, Error Code is %d\n", ret);
            break;
        }

        //FIX:No PTS (Example: Raw H.264)
        //Simple Write PTS
        if (packet.pts == AV_NOPTS_VALUE)
        {
            //Write PTS
            AVRational time_base1 = m_inputContext->streams[m_videoStreamIndex[0]]->time_base;
            //Duration between 2 frames (us)
            int64_t calc_duration =
                    (double) AV_TIME_BASE / av_q2d(m_inputContext->streams[m_videoStreamIndex[0]]->r_frame_rate);
            //Parameters
            packet.pts = (double) (videoWriteFrameCount * calc_duration) / (double) (av_q2d(time_base1) * AV_TIME_BASE);
            packet.dts = packet.pts;
            packet.duration = (double) calc_duration / (double) (av_q2d(time_base1) * AV_TIME_BASE);
        }

        /// 延迟发送,否则会出错
        Delay(packet, start_time);

        inStream = m_inputContext->streams[packet.stream_index];
        outStream = m_outputContext->streams[packet.stream_index];
        /* copy packet */
        //转换PTS/DTS(Convert PTS/DTS)
        packet.pts = av_rescale_q_rnd(packet.pts, inStream->time_base, outStream->time_base,
                                      (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        packet.dts = av_rescale_q_rnd(packet.dts, inStream->time_base, outStream->time_base,
                                      (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        packet.duration = av_rescale_q(packet.duration, inStream->time_base, outStream->time_base);
        packet.pos = -1;

        //Print to Screen
        if (packet.stream_index == m_videoStreamIndex[0])
        {
            LOG("Send %8d video frames to output URL\n", videoWriteFrameCount);
            ++videoWriteFrameCount;
        }

        ret = av_interleaved_write_frame(m_outputContext, &packet);

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

        av_free_packet(&packet);

    }
    //写文件尾
    av_write_trailer(m_outputContext);

    return 0;
}

值得注意的是,如果不执行延迟的操作,数据会过快发送给服务器,在播放的时候,画面一闪而过就没有了。
延迟的操作,尚且看懂,但是时间戳的计算方法就我现在还没搞懂,有时间再研究一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章