直播入門(四)通過代碼實現一個簡單的推流器

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

值得注意的是,如果不執行延遲的操作,數據會過快發送給服務器,在播放的時候,畫面一閃而過就沒有了。
延遲的操作,尚且看懂,但是時間戳的計算方法就我現在還沒搞懂,有時間再研究一下。

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