参考:最简单的基于FFmpeg的推流器(以推送RTMP为例)
====== 示例工程 ======
【FFmpeg】调用FFmpeg库实现264软编
【FFmpeg】调用FFmpeg库实现264软解
参考: Windows搭建RTMP服务器+OBS推流+VLC拉流
本文使用FFmpeg-7.0版本
将本机配置成为服务器,实现本地的推流和拉流操作。RTMP服务器的搭建参考:RTMP服务器的搭建
RTMP是Adobe提出的一种应用层的协议,用于解决多媒体数据传输流的多路复用(Multiplexing)和分包(packetizing)的问题传输,传输媒体的格式为FLV,因此本文推流的格式是flv格式。flv文件可以使用ffmpeg命令行,从yuv文件转换而来。
在实现时,对参考博客中的部分函数进行了修改
在编码过程当中,主要使用了如下的函数
函数名 | 作用 |
---|---|
av_log_set_level | 配置输出日志级别 (AV_LOG_TRACE最详细) |
avformat_network_init | 初始化网络模块 |
avformat_open_input | 打开输入文件,并且将文件信息赋值给AVFormatContext保存 |
avformat_find_stream_info | 根据AVFormatContext查找流信息 |
av_dump_format | 将AVFormatContext中的媒体文件的信息进行格式化输出 |
avformat_alloc_output_context2 | 根据format_name(或filename或oformat)创建输出文件的AVFormatContext信息 |
avformat_new_stream | 根据AVFormatContext和AVCodecContext创建新的流 |
avcodec_parameters_copy | 拷贝AVCodecParameters |
avio_open | 根据url进行AVIOContext的创建与初始化(这个url在推流时就是服务器地址) |
avformat_write_header | 为流分配priv_data并且将流的头信息写入到输出媒体文件 |
av_read_frame | 根据AVFormatContext所提供的的信息读取一帧,存入AVPacket |
av_interleaved_write_frame | 以交错的方式将帧送入到媒体文件中 |
av_packet_unref | 释放AVPacket |
av_write_trailer | 将流的尾部写入到输出的媒体文件中,并且释放文件中的priv_data |
avformat_close_input | 释放AVFormatContext |
从使用的函数来看,主要的操作流程和数据流走向大约为:
在上述流程当中,利用avformat_open_input获取了头信息,知道了是否包含音频流和视频流,但是并不知道编解码格式,还需要使用avformat_find_stream来获取具体的格式。
在调试时,发现如果AVPacket这里定义如果是指针的话,会出现av_read_frame第二帧读取失败的情况,这里有待进一步思考。
在代码中,利用bool is_sender来控制是发送还是接收,发送和接收都使用同一套代码,只是在时间戳部分有所区别,即发送端需要计算而接收端不需要使用。不过这里的in_url和out_url还是固定的,实际使用时得重新配置。
#pragma warning(disable : 4996)
#include
#include
#include
#include "streamer.h"
#ifdef _WIN32
//Windows
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#include "libavutil/opt.h"
#include "libavutil/time.h"
#include "libavutil/timestamp.h"
#include "libavutil/mathematics.h"
#include "libavutil/log.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#include "libavutil/opt.h"
#ifdef __cplusplus
};
#endif
#endif
int streamer_internal(const char* in_url, const char* out_url, bool is_sender)
{
// set log level
// av_log_set_level(AV_LOG_TRACE);
AVOutputFormat* av_out_fmt = NULL;
AVFormatContext* av_in_fmt_ctx = NULL;
AVFormatContext* av_out_fmt_ctx = NULL;
AVPacket av_pkt;
const char* in_filename = in_url;
const char* out_filename = out_url;
int ret = 0;
int i = 0;
int video_idx = -1;
int frame_idx = 0;
int64_t start_time = 0;
// bool b_sender = 0;
//in_filename = "enc_in_all.flv"; // input flv file
//out_filename = "rtmp://127.0.0.1:1935/live/stream"; // output url
//in_filename = "rtmp://127.0.0.1:1935/live/stream"; // input flv file
//out_filename = "receive.flv"; // output url
// av_register_all(); // 新版本ffmpeg不再使用
// init network
avformat_network_init();
if ((ret = avformat_open_input(&av_in_fmt_ctx, in_filename, 0, 0)) < 0) {
fprintf(stderr, "Could not open input file.");
goto end;
}
if ((ret = avformat_find_stream_info(av_in_fmt_ctx, 0)) < 0) {
fprintf(stderr, "Failed to retrive input stream information");
goto end;
}
for (i = 0; i < av_in_fmt_ctx->nb_streams; i++) {
if (av_in_fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_idx = i;
break;
}
}
// 将AVFormatContext结构体中媒体文件的信息进行格式化输出
av_dump_format(av_in_fmt_ctx, 0, in_filename, 0);
// Output
// av_out_fmt_ctx是函数执行成功之后的上下文信息结构体
// "flv"是输出格式
// out_filename是输出文件
ret = avformat_alloc_output_context2(&av_out_fmt_ctx, NULL, NULL, out_filename); // RTMP
// ret = avformat_alloc_output_context2(&av_out_fmt_ctx, NULL, "flv", out_filename); // RTMP
// avformat_alloc_output_context2(&av_out_fmt_ctx, NULL, "mpegts", out_filename); // UDP
if (ret < 0) {
fprintf(stderr, "Could not create output context, error code:%d\n", ret);
//ret = AVERROR_UNKNOWN;
goto end;
}
// av_out_fmt_ctx->oformat;
for (i = 0; i < av_in_fmt_ctx->nb_streams; i++) {
AVStream* in_stream = av_in_fmt_ctx->streams[i];
// 为av_out_fmt_ctx创建一个新的流,第二个参数video_codec没有被使用
AVStream* out_stream = avformat_new_stream(av_out_fmt_ctx, av_in_fmt_ctx->video_codec);
//AVStream* out_stream = avformat_new_stream(av_out_fmt_ctx, in_stream->codec->codec);
if (!out_stream) {
fprintf(stderr, "Failed to allocating output stream\n");
ret = AVERROR_UNKNOWN;
goto end;
}
// Copy the setting of AVCodecContext
// ret = avcodec_copy_context(out_stream->codecpar, in_stream->codecpar);
ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
if (ret < 0) {
fprintf(stderr, "Failed to copy context from input to output stream codec context\n");
goto end;
}
out_stream->codecpar->codec_tag = 0;
if (av_out_fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
// out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
// out_stream->event_flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
}
// Dump format
// 将AVFormatContext结构体中媒体文件的信息进行格式化输出
av_dump_format(av_out_fmt_ctx, 0, out_filename, 1);
// Open output URL
if (!(av_out_fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
// 打开文件
ret = avio_open(&av_out_fmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
if (ret < 0) {
fprintf(stderr, "Could not open output URL '%s'", out_filename);
goto end;
}
}
// Write file header
ret = avformat_write_header(av_out_fmt_ctx, NULL);
if (ret < 0) {
fprintf(stderr, "Error occured when opening output URL\n");
goto end;
}
if (is_sender) {
start_time = av_gettime();
}
while(1) {
AVStream* in_stream;
AVStream* out_stream;
// get an AVPacket
// 这里如果使用av_pkt指针的话,第二帧时就会出错
ret = av_read_frame(av_in_fmt_ctx, &av_pkt);
if (ret < 0) {
break;
}
// write pts
if (av_pkt.pts == AV_NOPTS_VALUE && is_sender) {
// write pts
AVRational time_base1 = av_in_fmt_ctx->streams[video_idx]->time_base;
// Duration between 2 frames (us)
int64_t calc_duration = (double)AV_TIME_BASE / av_q2d(av_in_fmt_ctx->streams[video_idx]->r_frame_rate);
// parameters
// pts是播放时间戳,告诉播放器什么时候播放这一帧视频,PTS通常是按照递增顺序排列的,以保证正确的时间顺序和播放同步
// dts是解码时间戳,告诉播放器什么时候解码这一帧视频
av_pkt.pts = (double)(frame_idx * calc_duration) / (double)(av_q2d(time_base1) * AV_TIME_BASE);
av_pkt.dts = av_pkt.pts;
av_pkt.duration = (double)calc_duration / (double)(av_q2d(time_base1) * AV_TIME_BASE);
}
// important: delay
if (av_pkt.stream_index == video_idx && is_sender) {
AVRational time_base = av_in_fmt_ctx->streams[video_idx]->time_base;
AVRational time_base_q = { 1, AV_TIME_BASE };
int64_t pts_time = av_rescale_q(av_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);
}
// av_usleep(50);
}
in_stream = av_in_fmt_ctx->streams[av_pkt.stream_index];
out_stream = av_out_fmt_ctx->streams[av_pkt.stream_index];
// copy packet
// convert PTS/DTS
av_pkt.pts = av_rescale_q_rnd(av_pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
av_pkt.dts = av_rescale_q_rnd(av_pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
av_pkt.duration = av_rescale_q(av_pkt.duration, in_stream->time_base, out_stream->time_base);
av_pkt.pos = -1;
// print to screen
if (av_pkt.stream_index == video_idx) {
if (is_sender) {
fprintf(stdout, "Send %8d video frames to output URL\n", frame_idx);
}
else {
fprintf(stdout, "Receive %8d video frames from input URL\n", frame_idx);
}
frame_idx++;
}
ret = av_interleaved_write_frame(av_out_fmt_ctx, &av_pkt);
// ret = av_write_frame(av_out_fmt_ctx, av_pkt);
if (ret < 0) {
fprintf(stderr, "Error muxing packet, error code:%d\n", ret);
break;
}
// av_packet_free(&av_pkt);
av_packet_unref(&av_pkt);
}
// write file trailer
av_write_trailer(av_out_fmt_ctx);
end:
avformat_close_input(&av_in_fmt_ctx);
// close output
/*if (av_out_fmt_ctx && !(av_out_fmt->flags & AVFMT_NOFILE)) {
avio_close(av_out_fmt_ctx->pb);
}*/
/*avformat_free_context(av_out_fmt_ctx);
if (ret < 0 && ret != AVERROR_EOF) {
fprintf(stderr, "Error occured\n");
return -1;
}*/
return 0;
}
int streamer()
{
const char* in_url = "rtmp://127.0.0.1:1935/live/stream"; // input flv file
const char* out_url = "receive.flv"; // output url
bool is_sender = 0;
streamer_internal(in_url, out_url, is_sender);
return 0;
}
使用代码进行推流,可以访问http://localhost/stat地址查看推流的状态。
...
...
Metadata:
encoder : Lavf61.3.100
Duration: 00:00:40.00, start: 0.000000, bitrate: 18849 kb/s
Stream #0:0: Video: h264 (High), yuv420p(progressive), 1920x1200, 25 fps, 25 tbr, 1k tbn
Output #0, flv, to 'rtmp://127.0.0.1:1935/live/stream':
Stream #0:0: Video: h264 (High), yuv420p(progressive), 1920x1200, q=2-31
Send 0 video frames to output URL
Send 1 video frames to output URL
Send 2 video frames to output URL
Send 3 video frames to output URL
Send 4 video frames to output URL
Send 5 video frames to output URL
Send 6 video frames to output URL
Send 7 video frames to output URL
Send 8 video frames to output URL
Send 9 video frames to output URL
Send 10 video frames to output URL
Send 11 video frames to output URL
Send 12 video frames to output URL
Send 13 video frames to output URL
Send 14 video frames to output URL
Send 15 video frames to output URL
Send 16 video frames to output URL
Send 17 video frames to output URL
Send 18 video frames to output URL
...
...
拉流时,需要对齐推流和拉流时的RTMP地址。如果不对齐,拉流将会一直处于idel状态。
Input #0, flv, from 'rtmp://127.0.0.1:1935/live/stream':
Metadata:
|RtmpSampleAccess: true
Server : NGINX RTMP (github.com/arut/nginx-rtmp-module)
displayWidth : 1920
displayHeight : 1200
fps : 25
profile :
level :
Duration: 00:00:00.00, start: 59.120000, bitrate: N/A
Stream #0:0: Video: h264 (High), yuv420p(progressive), 1920x1200, 25 fps, 25 tbr, 1k tbn
Output #0, flv, to 'receive.flv':
Stream #0:0: Video: h264 (High), yuv420p(progressive), 1920x1200, q=2-31
Receive 0 video frames from input URL
Receive 1 video frames from input URL
Receive 2 video frames from input URL
Receive 3 video frames from input URL
Receive 4 video frames from input URL
Receive 5 video frames from input URL
Receive 6 video frames from input URL
Receive 7 video frames from input URL
Receive 8 video frames from input URL
Receive 9 video frames from input URL
Receive 10 video frames from input URL
Receive 11 video frames from input URL
Receive 12 video frames from input URL
另外,推流和拉流也可以使用其他已有工具,例如推流直接使用ffmpeg.exe,拉流使用ffplay.exe(或VLC Media Player)
CSDN: https://blog.csdn.net/weixin_42877471
Github: https://github.com/DoFulangChen/