最简单的基于 FFmpeg 的推流器(以推送 RTMP 为例)
- 最简单的基于 FFmpeg 的推流器(以推送 RTMP 为例)
- 简介
- 需要注意的地方
- 封装格式
- 延时
- PTS/DTS问题
- 程序流程图
- 源程序
- 结果
- 工程文件下载
- 参考链接
最简单的基于 FFmpeg 的推流器(以推送 RTMP 为例)
参考雷霄骅博士的文章,链接:最简单的基于FFmpeg的推流器(以推送RTMP为例)
简介
本文记录一个最简单的基于 FFmpeg的 推流器(simplest ffmpeg streamer)。
推流器的作用就是将本地的视频数据推送至流媒体服务器。本文记录的推流器,可以将本地的 MOV / AVI / MKV / MP4 / FLV 等格式的媒体文件,通过流媒体协议(例如 RTMP,HTTP,UDP,TCP,RTP 等等)以直播流的形式推送出去。由于流媒体协议种类繁多,不一一记录。在这里记录将本地文件以 RTMP 直播流的形式推送至 RTMP 流媒体服务器(例如 Flash Media Server,Red5,Wowza 等等)的方法。
在这个推流器的基础上可以进行多种方式的修改,实现各式各样的推流器。例如:
- 将输入文件改为网络流 URL,可以实现转流器。
- 将输入的文件改为回调函数(内存读取)的形式,可以推送内存中的视频数据。
- 将输入文件改为系统设备(通过 libavdevice),同时加上编码的功能,可以实现实时推流器(现场直播)。
PS:本程序并不包含视频转码的功能。
RTMP 推流器(Streamer)的在流媒体系统中的作用可以用下图表示。首先将视频数据以 RTMP 的形式发送到流媒体服务器端(Server,比如 FMS,Red5,Wowza 等),然后客户端(一般为 Flash Player)通过访问流媒体服务器就可以收看实时流了。
运行本程序之前需要先运行 RTMP 流媒体服务器,并在流媒体服务器上建立相应的 Application。有关流媒体服务器的操作不在本文的论述范围内,在此不再详述,可以参考链接:《Windows搭建RTMP视频流服务(Nginx服务器版)》。
本程序运行后,即可通过 RTMP 客户端(例如 Flash Player, FFplay 等等)收看推送的直播流。
需要注意的地方
封装格式
RTMP 采用的封装格式是 FLV。因此在指定输出流媒体的时候需要指定其封装格式为“flv”。
同理,其他流媒体协议也需要指定其封装格式。例如采用 UDP 推送流媒体的时候,可以指定其封装格式为“mpegts”。
延时
发送流媒体的数据的时候需要延时。不然的话,FFmpeg 处理数据速度很快,瞬间就能把所有的数据发送出去,流媒体服务器是接受不了的,因此需要按照视频实际的帧率发送数据。
本文记录的推流器在视频帧与帧之间采用了 av_usleep() 函数休眠的方式来延迟发送。这样就可以按照视频的帧率发送数据了。
参考代码如下:
//…
int64_t start_time=av_gettime();
while (1) {
//…
//Important:Delay
if(pkt.stream_index==videoindex){
AVRational time_base=ifmt_ctx->streams[videoindex]->time_base;
AVRational time_base_q={1,AV_TIME_BASE};
int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
int64_t now_time = av_gettime() - start_time;
if (pts_time > now_time)
av_usleep(pts_time - now_time);
}
//…
}
//…
PTS/DTS问题
没有封装格式的裸流(例如 H.264 裸流)是不包含 PTS、DTS 这些参数的。在发送这种数据的时候,需要自己计算并写入 AVPacket 的 PTS,DTS,duration 等参数。这里还没有深入研究,简单写了一点代码,如下所示:
//FIX:No PTS (Example: Raw H.264)
//Simple Write PTS
if(pkt.pts==AV_NOPTS_VALUE){
//Write PTS
AVRational time_base1=ifmt_ctx->streams[videoindex]->time_base;
//Duration between 2 frames (us)
int64_t calc_duration=(double)AV_TIME_BASE/av_q2d(ifmt_ctx->streams[videoindex]->r_frame_rate);
//Parameters
pkt.pts=(double)(frame_index*calc_duration)/(double)(av_q2d(time_base1)*AV_TIME_BASE);
pkt.dts=pkt.pts;
pkt.duration=(double)calc_duration/(double)(av_q2d(time_base1)*AV_TIME_BASE);
}
程序流程图
程序的流程图如下图所示:
可以看出和《 最简单的基于 FFmpeg 的封装格式转换器(无编解码)》中的封装格式转换器比较类似。它们之间比较明显的区别在于:
-
Streamer 输出为 URL。
-
Streamer 包含了延时部分。
源程序
// Simplest FFmpeg Streamer.cpp : 定义控制台应用程序的入口点。
//
/**
* 最简单的基于 FFmpeg 的推流器(推送 RTMP)
* Simplest FFmpeg Streamer (Send RTMP)
*
* 源程序:
* 雷霄骅 Lei Xiaohua
* leixiaohua1020@126.com
* 中国传媒大学/数字电视技术
* Communication University of China / Digital TV Technology
* http://blog.csdn.net/leixiaohua1020
*
* 修改:
* 刘文晨 Liu Wenchen
* 812288728@qq.com
* 电子科技大学/电子信息
* University of Electronic Science and Technology of China / Electronic and Information Science
* https://blog.csdn.net/ProgramNovice
*
* 本例子实现了推送本地视频至流媒体服务器(以 RTMP 为例)。
* 是使用 FFmpeg 进行流媒体推送最简单的教程。
*
* This example stream local media files to streaming media
* server (Use RTMP as example).
* It's the simplest FFmpeg streamer.
*/
#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
// 解决报错:'fopen': This function or variable may be unsafe.Consider using fopen_s instead.
#pragma warning(disable:4996)
// 解决报错:无法解析的外部符号 __imp__fprintf,该符号在函数 _ShowError 中被引用
#pragma comment(lib, "legacy_stdio_definitions.lib")
extern "C"
{
// 解决报错:无法解析的外部符号 __imp____iob_func,该符号在函数 _ShowError 中被引用
FILE __iob_func[3] = { *stdin, *stdout, *stderr };
}
#define __STDC_CONSTANT_MACROS
#ifdef _WIN32
// Windows
extern "C"
{
#include "libavformat/avformat.h"
#include "libavutil/mathematics.h"
#include "libavutil/time.h"
};
#else
// Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include <libavformat/avformat.h>
#include <libavutil/mathematics.h>
#include <libavutil/time.h>
#ifdef __cplusplus
};
#endif
#endif
int main(int argc, char* argv[])
{
AVOutputFormat *ofmt = NULL;
// Input AVFormatContext and Output AVFormatContext
AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
AVPacket pkt;
// Input file
const char in_filename[] = "cuc_ieschool.flv";
// Output RTMP URL
const char out_filename[] = "rtmp://127.0.0.1:1935/live/test";
// Output UDP URL
// const char*out_filename[] = "rtp://233.233.233.233:6666";
int ret;
int videoindex = -1;
int frame_index = 0;
int64_t start_time = 0;
av_register_all();
// Init network
avformat_network_init();
// 输入
ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0);
if (ret < 0)
{
printf("Could not open input file.\n");
goto end;
}
ret = avformat_find_stream_info(ifmt_ctx, 0);
if (ret < 0)
{
printf("Failed to retrieve input stream information.\n");
goto end;
}
for (unsigned int i = 0; i < ifmt_ctx->nb_streams; i++)
if (ifmt_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
videoindex = i;
break;
}
// Print some input information
av_dump_format(ifmt_ctx, 0, in_filename, 0);
// RTMP
avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", out_filename);
// UDP
// avformat_alloc_output_context2(&ofmt_ctx, NULL, "mpegts", out_filename);
if (ofmt_ctx == NULL)
{
printf("Could not create output context.\n");
ret = AVERROR_UNKNOWN;
goto end;
}
ofmt = ofmt_ctx->oformat;
for (unsigned int i = 0; i < ifmt_ctx->nb_streams; i++)
{
// 根据输入流创建输出流(Create output AVStream according to input AVStream)
AVStream *in_stream = ifmt_ctx->streams[i];
AVStream *out_stream = avformat_new_stream(ofmt_ctx, in_stream->codec->codec);
if (out_stream == NULL)
{
printf("Failed allocating output stream.\n");
ret = AVERROR_UNKNOWN;
goto end;
}
// 复制 AVCodecContext 的设置(Copy the settings of AVCodecContext)
ret = avcodec_copy_context(out_stream->codec, in_stream->codec);
if (ret < 0)
{
printf("Failed to copy context from input to output stream codec context.\n");
goto end;
}
out_stream->codec->codec_tag = 0;
if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
{
out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
}
}
// Print some output information
av_dump_format(ofmt_ctx, 0, out_filename, 1);
// Open output URL
if (!(ofmt->flags & AVFMT_NOFILE))
{
ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
if (ret < 0)
{
printf("Could not open output URL '%s'.\n", out_filename);
goto end;
}
}
// Write file header
ret = avformat_write_header(ofmt_ctx, NULL);
if (ret < 0)
{
printf("Error occurred when opening output URL.\n");
goto end;
}
start_time = av_gettime();
while (1)
{
AVStream *in_stream, *out_stream;
// 获取一个 AVPacket
ret = av_read_frame(ifmt_ctx, &pkt);
if (ret < 0)
{
break;
}
// FIX:No PTS (Example: Raw H.264)
// Simple Write PTS
if (pkt.pts == AV_NOPTS_VALUE)
{
// Write PTS
AVRational time_base1 = ifmt_ctx->streams[videoindex]->time_base;
// Duration between 2 frames (us)
int64_t calc_duration = (double)AV_TIME_BASE / av_q2d(ifmt_ctx->streams[videoindex]->r_frame_rate);
// Parameters
pkt.pts = (double)(frame_index * calc_duration) / (double)(av_q2d(time_base1) * AV_TIME_BASE);
pkt.dts = pkt.pts;
pkt.duration = (double)calc_duration / (double)(av_q2d(time_base1) * AV_TIME_BASE);
}
// Delay
if (pkt.stream_index == videoindex)
{
AVRational time_base = ifmt_ctx->streams[videoindex]->time_base;
AVRational time_base_q = { 1, AV_TIME_BASE };
int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
int64_t now_time = av_gettime() - start_time;
if (pts_time > now_time)
{
av_usleep(pts_time - now_time);
}
}
in_stream = ifmt_ctx->streams[pkt.stream_index];
out_stream = ofmt_ctx->streams[pkt.stream_index];
// copy packet
// 转换 PTS/DTS(Convert PTS/DTS)
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base,
(AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base,
(AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
pkt.pos = -1;
// Print to Screen
if (pkt.stream_index == videoindex)
{
printf("Send %8d video frames to output URL.\n", frame_index);
frame_index++;
}
// ret = av_write_frame(ofmt_ctx, &pkt);
ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
if (ret < 0)
{
printf("Error muxing packet.\n");
break;
}
av_free_packet(&pkt);
}
// Write file trailer
av_write_trailer(ofmt_ctx);
end:
avformat_close_input(&ifmt_ctx);
// Close output
if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
{
avio_close(ofmt_ctx->pb);
}
avformat_free_context(ofmt_ctx);
if (ret < 0 && ret != AVERROR_EOF)
{
printf("Error occurred.\n");
return -1;
}
system("pause");
return 0;
}
结果
按《Windows搭建RTMP视频流服务(Nginx服务器版)》 流程的配置 RTMP 服务器。
播放地址根据 nginx.conf 文件一一对应:
本文实现推流地址为:rtmp://127.0.0.1:1935/live/test
。
打开 CMD,进入 nginx 1.7.11.3 Gryphon 目录下,执行如下指令,检查 nginx 的配置文件是否正确。
启动 nginx 服务器:start nginx
,载入缺省 ./conf/nginx.conf 配置文件,启动 nginx。
若要采用别的配置去开启 nginx,命令如下:
nginx.exe -c conf\nginx-win.conf
。
运行程序,输出如下:
程序运行的同时,使用 VLC media player 打开网络串流,输入之前的地址。
播放直播流的截图如下图所示:
此外,也可以通过 PotPlayer/FFplay 这样的客户端播放直播流。
停止 nginx 服务器,命令如下:taskkill /f /im nginx.exe
。
工程文件下载
GitHub:UestcXiye / Simplest-FFmpeg-Streamer
CSDN:Simplest FFmpeg Streamer.zip
参考链接
- 最简单的基于 FFmpeg 的封装格式转换器(无编解码)
- Windows搭建RTMP视频流服务(Nginx服务器版)