前言
本文亲手操练直播项目,对其中的原理及源码进行解读和剖析。
一、直播模型与框架图
一个通用的直播模型一般包括三个模块:主播方、 服务器端和播放端。
一个描述数字音视频传输和播放流程的简单步骤:录制 -> 编码 -> 网络传输 -> 解码 -> 播放
- 首先是主播方,它是产生视频流的源头,由一系列流程组成:第一,通过一定的设备来采集数据;第二,将采集的这些视频进行一系列的处理,比如水印、美颜和特效滤镜等处理;第三,将处理后的结果视频编码压缩成可观看可传输的视频流;第四,分发推流,即将压缩后的视频流通过网络通道传输出去。
- 其次是播放端,播放端功能有两个层面,第一个层面是关键性的需求;另一层面是业务层面的。先看第一个层面, 它涉及到一些非常关键的指标,比如秒开,在很多场景当中都有这样的要求,然后是对于一些重要内容的版权保护。 为了达到更好的效果,我们还需要配合服务端做智能解析,这在某些场景下也是关键性需求。再来看第二个层面也即业务层面的功能,对于一个社交直播产品来说,在播放端,观众希望能够实时的看到主播端推过来的视频流,并且和主播以及其他观众产生一定的互动,因此它可能包含一些像点赞、聊天和弹幕这样的功能,以及礼物这样更高级的道具。
- 直播服务器端提供的最核心功能是收集主播端的视频推流,并将其放大后推送给所有观众端。除了这个核心功能,还有很多运营级别的诉求,比如鉴权认证,视频连线和实时转码,自动鉴黄,多屏合一,以及云端录制存储等功能。另外,对于一个主播端推出的视频流,中间需要经过一些环节才能到达播放端,因此对中间环节的质量进行监控,以及根据这些监控来进行智能调度,也是非常重要的诉求。
二、搭建 Nginx 直播服务器
具体搭建流程可以参考我之前的博客:Nginx直播服务器搭建及推拉流测试
三、推流拉流直播实战
下面的代码与下面的命令起到同样的功能:
ffmpeg -re -i test.flv -vcodec libx264 -acodec aac -f flv -y rtmp://192.168.137.128/live1/test1
即使用 FFmpeg 将名为 “test.flv” 的视频文件以 libx264 视频编解码器和 AAC 音频编解码器的格式流式传输到位于 “rtmp://192.168.137.128/live1/test1” 的 RTMP 服务器。
1、示例源码
#include <stdio.h>
#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;
//输入对应一个AVFormatContext,输出对应一个AVFormatContext
//(Input AVFormatContext and Output AVFormatContext)
AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
AVPacket pkt;
const char *in_filename, *out_filename;
int ret, i;
int videoindex=-1;
int frame_index=0;
int64_t start_time=0;
in_filename = "./debug/test.flv";
out_filename = "rtmp://192.168.137.128/live1/test1";//输出 URL(Output URL)[RTMP]
//Network
avformat_network_init();
//输入(Input)
if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) {
printf( "Could not open input file.");
goto end;
}
if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
printf( "Failed to retrieve input stream information");
goto end;
}
for(i=0; i<ifmt_ctx->nb_streams; i++)
if(ifmt_ctx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){
videoindex=i;
break;
}
av_dump_format(ifmt_ctx, 0, in_filename, 0);
//输出(Output)
avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", out_filename); //RTMP
//avformat_alloc_output_context2(&ofmt_ctx, NULL, "mpegts", out_filename);//UDP
if (!ofmt_ctx) {
printf( "Could not create output context\n");
ret = AVERROR_UNKNOWN;
goto end;
}
ofmt = ofmt_ctx->oformat;
for (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) {
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;
}
//Dump Format------------------
av_dump_format(ofmt_ctx, 0, out_filename, 1);
//打开输出URL(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'", 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(Get an 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);
}
//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);
}
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);
avformat_network_deinit();
/* 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;
}
return 0;
}
以下是对相关 API 接口的讲解:
int avformat_open_input(AVFormatContext **ps, const char *url, ff_const59 AVInputFormat *fmt, AVDictionary **options);
// 打开输入媒体文件的函数- 使用 avformat_open_input() 函数可以打开媒体文件,并将相关信息存储在 AVFormatContext 结构体中,以便后续进行媒体文件的解码、编码或其他处理操作。
- ps: 指向指向 AVFormatContext 结构体指针的指针。该函数会为该指针分配内存并将打开的媒体文件的信息存储在其中。
- url: 表示要打开的媒体文件的 URL 或文件路径。
- fmt: 指定强制使用的输入格式。一般情况下,可以传入NULL,由 FFmpeg 库自动检测并选择适合的输入格式。
- options: 可选参数字典,用于传递额外的选项给输入格式的处理器。
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
// 用于获取媒体文件流信息- 调用 avformat_find_stream_info() 函数会读取媒体文件中的流信息,并将这些信息存储在 AVFormatContext 结构体中的 streams 成员中。这些流信息包括音频流、视频流、字幕流等的相关参数,如编码格式、时长、码率等。
- ic:指向已经通过 avformat_open_input() 函数打开的 AVFormatContext 结构体指针。
- options:可选参数字典,用于传递额外的选项给媒体文件的解封装器。
void av_dump_format(AVFormatContext *ic, int index, const char *url, int is_output);
// 打印媒体文件格式信息的函数- 调用 av_dump_format() 函数会打印媒体文件的详细信息,包括媒体文件的格式、流信息、编码参数、时长、码率等。通过观察输出结果,可以获取媒体文件的结构和参数信息,以便进行后续的处理和操作。
- ic:指向已经通过 avformat_open_input() 或 avformat_alloc_output_context2() 函数打开或创建的 AVFormatContext 结构体指针。
- index:指定要打印信息的流的索引,如果设置为-1,则打印所有流的信息。
- url:媒体文件的 URL 或文件路径。
- is_output:指示是否为输出(编码)上下文。如果为非零值,则表示为输出上下文,打印的是输出格式的信息。
int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat, const char *format_name, const char *filename);
// 分配输出格式上下文的函数,用于创建新的媒体文件- 使用 avformat_alloc_output_context2() 函数可以分配一个输出格式上下文,用于创建新的媒体文件。该函数会根据指定的输出格式或文件扩展名,自动选择适合的输出格式。在分配完上下文后,可以通过该上下文进行媒体文件的编码、写入等操作。
- ctx:指向指向 AVFormatContext 结构体指针的指针。函数将分配内存并将输出格式上下文存储在其中。
- oformat:指定要使用的输出格式,一般可以传入 NULL,由 FFmpeg 库自动选择适合的输出格式。
- format_name:输出格式的名称,例如 “mp4”、“avi” 等。如果指定了 oformat,该参数可以为 NULL。
- filename:要创建的媒体文件的文件名或输出 URL。
AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c);
// 创建新的媒体流的函数- 使用 avformat_new_stream() 函数可以在输出格式上下文中创建一个新的媒体流。可以在创建流后对其进行配置,如指定编码器、设置流的参数等。函数返回值为指向新创建的 AVStream 结构体的指针,表示新的媒体流。
- s:指向已经通过 avformat_alloc_output_context2() 函数分配的 AVFormatContext 结构体指针。
- c:指向 AVCodec 结构体的指针,表示要关联的编码器。可以传入 NULL,在后续的流配置中再设置编码器。
int avcodec_copy_context(AVCodecContext *dest, const AVCodecContext *src);
// 用于复制编解码器参数的函数- 将源 AVCodecContext 结构体中的设置和参数复制到目标 AVCodecContext 结构体中,使得目标结构体与源结构体具有相同的设置和参数
- dest:目标 AVCodecContext 结构体指针,用于接收复制后的设置和参数。
- src:源 AVCodecContext 结构体指针,用于提供要复制的设置和参数。
int avio_open(AVIOContext **s, const char *url, int flags);
// 打开一个输入或输出的媒体文件- 用于创建一个 AVIOContext 结构体,它提供了对媒体文件的读取或写入功能。函数根据指定的 URL 或文件路径打开媒体文件,并将打开后的上下文赋值给 AVIOContext 结构体的指针。
- s:指向 AVIOContext 指针的指针。在函数调用成功后,指针将指向一个用于访问媒体文件的 AVIOContext 结构体。
- url:要打开的媒体文件的 URL 或文件路径。
- flags:打开文件的标志位,用于指定打开方式和访问权限等。常见的标志位包括 AVIO_FLAG_READ(只读模式)、AVIO_FLAG_WRITE(只写模式)和 AVIO_FLAG_READ_WRITE(读写模式)等。
int avformat_write_header(AVFormatContext *s, AVDictionary **options);
// 写入媒体文件的头部信息- 该函数在打开媒体文件并设置好输出格式后,将写入媒体文件的头部信息。头部信息包括文件格式、编码器信息、流信息等。函数在写入完头部信息后,会初始化媒体文件的写入状态。成功写入头部信息则返回 0;否则返回负数错误代码。
- s:指向 AVFormatContext 结构体的指针,表示要写入头部信息的媒体文件上下文。
- options:指向 AVDictionary 指针的指针,用于传递附加的选项参数。可以传递一些配置选项,如全局元数据、流元数据等。可以将其设置为 NULL,如果不需要传递额外的选项。
int64_t av_gettime(void);
// 用于获取当前系统时钟的时间- 通常用于计算时间间隔、计时等需要获取精确时间的场景。可以使用该函数获取时间戳,并计算时间差来实现一些时间相关的逻辑。
- 该函数返回一个 int64_t 类型的值,表示当前系统时钟的时间。返回的时间单位是微秒(us)。
int av_read_frame(AVFormatContext *s, AVPacket *pkt);
// 从输入文件或流中读取下一个可用的帧- 该函数的作用是从输入文件或流中读取下一个可用的帧,并将其存储在提供的 AVPacket 结构体中
- s:指向 AVFormatContext 结构体的指针,表示输入文件或流的格式上下文。
- pkt:指向 AVPacket 结构体的指针,用于接收读取的帧数据。
av_q2d(AVRational a)
// 将一个 AVRational 类型的分数(有理数)转换为一个 double 类型的浮点数int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq);
// 用于将一个数值按照给定的时间基进行缩放,参数 a 是待缩放的数值,bq 是当前数值的时间基,cq 是目标时间基- 函数内部通过将数值 a 乘以 bq 的分子(num)和 cq 的分母(den)的商,再除以 bq 的分母(den)和 cq 的分子(num)的商,来实现数值的缩放。
- 这个函数在音视频处理中经常用于时间单位的转换,比如将以一种时间基表示的时间戳转换为以另一种时间基表示的时间戳,或者将以一种时间基表示的时长转换为以另一种时间基表示的时长。
int64_t av_gettime(void);
// 用于获取当前系统的时钟时间- av_gettime 函数返回一个 int64_t 类型的值,表示当前系统的时钟时间。该时间通常以微秒(μs)为单位
- av_gettime 函数可以用于测量时间间隔、计算运行时间、进行时间戳的处理等。它提供了高精度的系统时间,可用于多媒体处理中需要精确时间计算和同步的场景
int64_t av_rescale_q_rnd(int64_t a, AVRational bq, AVRational cq, enum AVRounding rnd);
// 用于将一个数值按照给定的时间基进行缩放,但具有额外的舍入(rounding)选项- 参数 a 是待缩放的数值,bq 是当前数值的时间基,cq 是目标时间基,rnd 是舍入模式
- AV_ROUND_ZERO:向最接近的零方向舍入
- AV_ROUND_INF:向最接近的整数方向舍入
- AV_ROUND_DOWN:向负无穷大方向舍入
- AV_ROUND_UP:向正无穷大方向舍入
- AV_ROUND_NEAR_INF:向最接近的整数方向舍入,如果距离两个整数一样,则向偶数方向舍入
- AV_ROUND_PASS_MINMAX:用于在数值舍入时以最小值和最大值为界限进行处理
int av_interleaved_write_frame(AVFormatContext *s, AVPacket *pkt);
// 用于将编码后的音视频帧写入容器文件- 函数的作用是将 AVPacket 中存储的音视频帧数据写入到容器文件中。函数会根据音视频帧的时间戳信息,将帧按照正确的时间顺序进行写入,以保持音视频的同步性。
- s:包含了与容器相关的信息,如文件名、封装格式、编码参数等
- pkt:AVPacket 用于存储编码后的音视频数据,包括数据缓冲区指针、数据大小、时间戳等信息
2、运行结果
打印信息如下:
使用 VLC 拉流
点击播放可以看到推向服务器的音视频流