FFmpeg版本:4.1.3
原理
從前面的文章,我們可以知道,實現推流客戶端需要執行的下面幾個步驟:
- 採集
- 編碼
- 封裝
- 推流
本文實現的是將本地的文件推送到服務器的過程,因此,不存在採集和編碼過程。只有封裝和推流的過程。
代碼分析
源代碼在 :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;
}
值得注意的是,如果不執行延遲的操作,數據會過快發送給服務器,在播放的時候,畫面一閃而過就沒有了。
延遲的操作,尚且看懂,但是時間戳的計算方法就我現在還沒搞懂,有時間再研究一下。