最简单的基于 FFmpeg 的推流器(以推送 RTMP 为例)

最简单的基于 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 的封装格式转换器(无编解码)》中的封装格式转换器比较类似。它们之间比较明显的区别在于:

  1. Streamer 输出为 URL。

  2. 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

参考链接

  1. 最简单的基于 FFmpeg 的封装格式转换器(无编解码)
  2. Windows搭建RTMP视频流服务(Nginx服务器版)
  • 30
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值