1.1 FFmpeg 影音处理的层次

FFmpeg 中对影音数据的处理,可以划分为协议层、容器层、编码层与原始数据层四个层次:

协议层:供应网络协议收发功能,可以吸收或推送含封装格式的媒体流。
协议层由 libavformat 库及第三方库(如 librtmp)供应支持。

phpffmpeg推流源码FFmpeg流媒体处置收流与推流 GraphQL

容器层:处理各种封装格式。
容器层由 libavformat 库供应支持。

编码层:处理音视频编码及解码。
编码层由各种丰富的编解码器(libavcodec 库及第三方编解码库(如 libx264))供应支持。

原始数据层:处理未编码的原始音视频帧。
原始数据层由各种丰富的音视频滤镜(libavfilter 库)供应支持。

本文提及的收流与推流的功能,属于协议层的处理。

FFmpeg 中 libavformat 库供应了丰富的协议处理及封装格式处理功能,在打开输入/输出时,FFmpeg 会根据 输入 URL / 输出 URL 探测输入/输出格式,选择得当的协议和封装格式。
例如,如果输出 URL 是 "rtmp://192.168.0.104/live",那么 FFmpeg 打开输出时,会确定利用 rtmp 协议,封装格式为 flv。

FFmpeg 中打开输入/输出的内部处理细节用户不必关注,因此本文流处理的例程和前面转封装的例程非常相似,不同之处紧张在于输入/输出 URL 形式不同,若 URL 携带 "rtmp://"、"rpt://"、"udp://"等前缀,则表示涉及流处理;否则,处理的是本地文件。

1.2 流媒体系统中的角色

流媒体系统是一个比较繁芜的系统,大略来说涉及三个角色:流媒体做事器、推流客户端和收流客户端。
推流客户端是内容生产者,收流客户端是内容消费者。
示意图如下:

1.3 收流与推流

如果输入是网络流,输出是本地文件,则实现的是收流功能,将网络流存储为本地文件,如下:

如果输入是本地文件,输出是网络流,则实现的是推流功能,将本地文件推送到网络,如下:

如果输入是网络流,输出也是网络流,则实现的是转流功能,将一个流媒体做事器上的流推送到另一个流媒体做事器,如下:

领取音视频开拓资料包:音视频流媒体高等开拓FFmpegWebRTCRTMPRTSPHLSRTP播放器

企鵝君羊994289133领取资料

企鵝君羊994289133领取资料

2. 源码

源码和转封装例程大部分相同,可以认为是转封装例程的增强版:

#include <stdbool.h>#include <libavutil/timestamp.h>#include <libavformat/avformat.h>// ffmpeg -re -i tnhaoxc.flv -c copy -f flv rtmp://192.168.0.104/live// ffmpeg -i rtmp://192.168.0.104/live -c copy tnlinyrx.flv// ./streamer tnhaoxc.flv rtmp://192.168.0.104/live// ./streamer rtmp://192.168.0.104/live tnhaoxc.flvint main(int argc, char argv){ AVOutputFormat ofmt = NULL; AVFormatContext ifmt_ctx = NULL, ofmt_ctx = NULL; AVPacket pkt; const char in_filename, out_filename; int ret, i; int stream_index = 0; int stream_mapping = NULL; int stream_mapping_size = 0; if (argc < 3) { printf("usage: %s input output\n" "API example program to remux a media file with libavformat and libavcodec.\n" "The output format is guessed according to the file extension.\n" "\n", argv[0]); return 1; } in_filename = argv[1]; out_filename = argv[2]; // 1. 打开输入 // 1.1 读取文件头,获取封装格式干系信息 if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) { printf("Could not open input file '%s'", in_filename); goto end; } // 1.2 解码一段数据,获取流干系信息 if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) { printf("Failed to retrieve input stream information"); goto end; } av_dump_format(ifmt_ctx, 0, in_filename, 0); // 2. 打开输出 // 2.1 分配输出ctx bool push_stream = false; char ofmt_name = NULL; if (strstr(out_filename, "rtmp://") != NULL) { push_stream = true; ofmt_name = "flv"; } else if (strstr(out_filename, "udp://") != NULL) { push_stream = true; ofmt_name = "mpegts"; } else { push_stream = false; ofmt_name = NULL; } avformat_alloc_output_context2(&ofmt_ctx, NULL, ofmt_name, out_filename); if (!ofmt_ctx) { printf("Could not create output context\n"); ret = AVERROR_UNKNOWN; goto end; } stream_mapping_size = ifmt_ctx->nb_streams; stream_mapping = av_mallocz_array(stream_mapping_size, sizeof(stream_mapping)); if (!stream_mapping) { ret = AVERROR(ENOMEM); goto end; } ofmt = ofmt_ctx->oformat; AVRational frame_rate; double duration; for (i = 0; i < ifmt_ctx->nb_streams; i++) { AVStream out_stream; AVStream in_stream = ifmt_ctx->streams[i]; AVCodecParameters in_codecpar = in_stream->codecpar; if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO && in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO && in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) { stream_mapping[i] = -1; continue; } if (push_stream && (in_codecpar->codec_type == AVMEDIA_TYPE_VIDEO)) { frame_rate = av_guess_frame_rate(ifmt_ctx, in_stream, NULL); duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0); } stream_mapping[i] = stream_index++; // 2.2 将一个新流(out_stream)添加到输出文件(ofmt_ctx) out_stream = avformat_new_stream(ofmt_ctx, NULL); if (!out_stream) { printf("Failed allocating output stream\n"); ret = AVERROR_UNKNOWN; goto end; } // 2.3 将当前输入流中的参数拷贝到输出流中 ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar); if (ret < 0) { printf("Failed to copy codec parameters\n"); goto end; } out_stream->codecpar->codec_tag = 0; } av_dump_format(ofmt_ctx, 0, out_filename, 1); if (!(ofmt->flags & AVFMT_NOFILE)) { // TODO: 研究AVFMT_NOFILE标志 // 2.4 创建并初始化一个AVIOContext,用以访问URL(out_filename)指定的资源 ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE); if (ret < 0) { printf("Could not open output file '%s'", out_filename); goto end; } } // 3. 数据处理 // 3.1 写输出文件头 ret = avformat_write_header(ofmt_ctx, NULL); if (ret < 0) { printf("Error occurred when opening output file\n"); goto end; } while (1) { AVStream in_stream, out_stream; // 3.2 从输出流读取一个packet ret = av_read_frame(ifmt_ctx, &pkt); if (ret < 0) { break; } in_stream = ifmt_ctx->streams[pkt.stream_index]; if (pkt.stream_index >= stream_mapping_size || stream_mapping[pkt.stream_index] < 0) { av_packet_unref(&pkt); continue; } int codec_type = in_stream->codecpar->codec_type; if (push_stream && (codec_type == AVMEDIA_TYPE_VIDEO)) { av_usleep((int64_t)(durationAV_TIME_BASE)); } pkt.stream_index = stream_mapping[pkt.stream_index]; out_stream = ofmt_ctx->streams[pkt.stream_index]; / copy packet / // 3.3 更新packet中的pts和dts // 关于AVStream.time_base(容器中的time_base)的解释: // 输入:输入流中含有time_base,在avformat_find_stream_info()中可取到每个流中的time_base // 输出:avformat_write_header()会根据输出的封装格式确定每个流的time_base并写入文件中 // AVPacket.pts和AVPacket.dts的单位是AVStream.time_base,不同的封装格式AVStream.time_base不同 // 以是输出文件中,每个packet须要根据输出封装格式重新打算pts和dts av_packet_rescale_ts(&pkt, in_stream->time_base, out_stream->time_base); pkt.pos = -1; // 3.4 将packet写入输出 ret = av_interleaved_write_frame(ofmt_ctx, &pkt); if (ret < 0) { printf("Error muxing packet\n"); break; } av_packet_unref(&pkt); } // 3.5 写输出文件尾 av_write_trailer(ofmt_ctx);end: avformat_close_input(&ifmt_ctx); / close output / if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE)) { avio_closep(&ofmt_ctx->pb); } avformat_free_context(ofmt_ctx); av_freep(&stream_mapping); if (ret < 0 && ret != AVERROR_EOF) { printf("Error occurred: %s\n", av_err2str(ret)); return 1; } return 0;}

2.1 收流

收流的代码与打开普通文件的代码没有差异,打开输入时,FFmpeg 能识别流协议及封装格式,根据相应的协议层代码来吸收流,收到流数据去掉协议层后得到的数据和普通文件内容是一样的,后续的处理流程也就一样了。

2.2 推流

推流有两个须要把稳的地方。

一是须要根据输出流协议显式指定输出 URL 的封装格式:

bool push_stream = false; char ofmt_name = NULL; if (strstr(out_filename, "rtmp://") != NULL) { push_stream = true; ofmt_name = "flv"; } else if (strstr(out_filename, "udp://") != NULL) { push_stream = true; ofmt_name = "mpegts"; } else { push_stream = false; ofmt_name = NULL; } avformat_alloc_output_context2(&ofmt_ctx, NULL, ofmt_name, out_filename);

这里只写了两种。
rtmp 推流必须推送 flv 封装格式,udp 推流必须推送 mpegts 封装格式,其他情形就当作是输出普通文件。
这里利用 push_stream 变量来标志是否利用推流功能,这个标志后面会用到。

二是要把稳推流的速率,不能一股脑将收到的数据全推出去,这样流媒体做事器承受不住。
可以按视频播放速率(帧率)来推流。
因此每推送一个视频帧,要延时一个视频帧的时长。
音频流的数据量很小,可以不必关心此问题。

在打开输入 URL 时,获取视频帧的持续时长:

AVRational frame_rate; double duration; if (push_stream && (in_codecpar->codec_type == AVMEDIA_TYPE_VIDEO)) { frame_rate = av_guess_frame_rate(ifmt_ctx, in_stream, NULL); duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0); }

在 av_read_frame() 之后,av_interleaved_write_frame() 之前增加延时,延时时长便是一个视频帧的持续时长:

int codec_type = in_stream->codecpar->codec_type; if (push_stream && (codec_type == AVMEDIA_TYPE_VIDEO)) { av_usleep((int64_t)(durationAV_TIME_BASE)); }3. 验证

3.1 编译第三方库 librtmp

FFmpeg 默认并不支持 rtmp 协议。
须要先编译安装第三方库 librtmp,然后开启 --enable-librtmp 选项重新编译安装 FFmpeg。

3.2 搭建流媒体做事器

测试收流与推流功能须要搭建流媒体做事器。
我们选用 nginx-rtmp 作为流媒体做事器用于测试。
nginx-rtmp 做事器运行于虚拟机上,推流客户端与收流客户端和 nginx-rtmp 做事器处于同一局域网即可。
我的虚拟机是 OPENSUSE LEAP 42.3,IP 是 192.168.0.104(便是 nginx-rtmp 做事器的地址)。

为避免搭建做事器的繁琐过程,我们直策应用 docker 拉取一个 nginx-rtmp 镜像。
步骤如下:

[1] 安装与配置docker做事

安装 docker:

sudo zypper install docker

将当前用户添加到 docker 组(若 docker 组不存在则先创建),从而可以免 sudo 利用 docker 命令:

sudo gpasswd -a ${USER} docker

[2] 配置镜像加速

docker 镜像源位于美国,摘取镜像非常缓慢。
可配置海内镜像源,加快镜像拉取速率。

修正 /etc/docker/daemon.json 文件并添加上 registry-mirrors 键值:

{ "registry-mirrors": [ "https://registry.docker-cn.com", "https://docker.mirrors.ustc.edu.cn", "https://hub-mirror.c.163.com", "https://mirror.ccs.tencentyun.com" ]}

上述配置文件添加了四个海内镜像源:docker 中国、清华、163 和腾讯。

修正配置文件后重启 docker 做事:

systemctl restart docker

[3] 拉取 nginx-rtmp 镜像

docker pull tiangolo/nginx-rtmp

[4] 打开容器

docker run -d -p 1935:1935 --name nginx-rtmp tiangolo/nginx-rtmp

[5] 防火墙添加例外端口

如果无法推流,应在防火墙中将 1935 端口添加例外

openSUSE 系统:修正 /etc/sysconfig/SuSEfirewall2 文件,在 FW_SERVICES_EXT_TCP 项中添加 1935 端口,如下:

FW_SERVICES_EXT_TCP="ssh 1935"

然后重启防火墙:

systemctl restart SuSEfirewall2

CentOS 8 系统:运行如下命令将 1935 端口添加到防火墙例外端口中:

firewall-cmd --permanent --zone=public --add-port=1935/tcp

[6] 验证做事器

测试文件下载(右键另存为):tnhaoxc.flv

ffmpeg 推流测试:

ffmpeg -re -i tnhaoxc.flv -c copy -f flv rtmp://192.168.0.104/live

"-re":按视频帧率的速率读取输入"-c copy":输出流利用和输入流相同的编解码器"-f flv":指定输出流封装格式为flv

ffplay 收流播放测试:

ffplay rtmp://192.168.0.104/live

ffplay 播放正常,解释 nginx-rtmp 流媒体做事器搭建成功,可正常利用。

3.3 编译

在 shell 中运行如下命令下载例程源码:

svn checkout https://github.com/leichn/exercises/trunk/source/ffmpeg/ffmpeg_stream

在源码目录实行 ./compile.sh 命令,天生 streamer 可实行文件。

3.4 验证

测试文件下载(右键另存为):shifu.mkv,将测试文件保存在和源码同一目录。

推流测试:

./streamer shifu.mkv rtmp://192.168.0.104/live

利用 vlc 播放器打开网络串流,输入流地址 "rtmp://192.168.0.104/live",播放正常。
上述测试命令等价于:

ffmpeg -re -i shifu.mkv -c copy -f flv rtmp://192.168.0.104/live

收流测试:先按照上一步命令启动推流,然后运行如下命令收流

./streamer rtmp://192.168.0.104/live shifu.ts

以上测试命令等价于:

ffmpeg -i rtmp://192.168.0.104/live -c copy shifu.ts

吸收结束后检讨一下天生确当地文件 shifu.ts 能否正常播放。