本文为做者原创,转载请注明出处:http://www.javashuo.com/article/p-xqiatytd-q.htmlhtml
流媒体是使用了流式传输的多媒体应用技术。以下是维基百科关于流媒体概念的定义:nginx
流媒体(streaming media)是指将一连串的媒体数据压缩后,通过网络分段发送数据,在网络上即时传输影音以供观赏的一种技术与过程,此技术使得数据包得以像流水同样发送;若是不使用此技术,就必须在使用前下载整个媒体文件。git
关于流媒体的基础概念,观止云的“流媒体|从入门到出家”系列文章极具参考价值,请参考本文第5节参考资料部分。github
流媒体系统是一个比较复杂的系统,简单来讲涉及三个角色:流媒体服务器、推流客户端和收流客户端。推流客户端是内容生产者,收流客户端是内容消费者。示意图以下:
docker
FFmpeg中对影音数据的处理,能够划分为协议层、容器层、编码层与原始数据层四个层次。协议层提供网络协议收发功能,能够接收或推送含封装格式的媒体流。协议层由libavformat库及第三方库(如librtmp)提供支持。容器层处理各类封装格式。容器层由libavformat库提供支持。编码层处理音视频编码及解码。编码层由各类丰富的编解码器(libavcodec库及第三方编解码库(如libx264))提供支持。原始数据层处理未编码的原始音视频帧。原始数据层由各类丰富的音视频滤镜(libavfilter库)提供支持。shell
本文说起的收流与推流的功能,属于协议层的处理。FFmpeg中libavformat库提供了丰富的协议处理及封装格式处理功能,在打开输入/输出时,FFmpeg会根据输入URL/输出URL探测输入/输出格式,选择合适的协议和封装格式。例如,若是输出URL是rtmp://192.168.0.104/live
,那么FFmpeg打开输出时,会肯定使用rtmp协议,封装格式为flv。FFmpeg中打开输入/输出的内部处理细节用户没必要关注,所以本文流处理的例程和前面转封装的例程很是类似,不一样之处主要在于输入/输出URL形式不一样,若URL携带“rtmp://”、“rpt://”、“udp://”等前缀,则表示涉及流处理;不然,处理的是本地文件。json
若是输入是网络流,输出是本地文件,则实现的是拉流功能,将网络流存储为本地文件,以下:
服务器
若是输入是本地文件,输出是网络流,则实现的是推流功能,将本地文件推送到网络,以下:
网络
若是输入是网络流,输出也是网络流,则实现的是转流功能,将一个流媒体服务器上的流推送到另外一个流媒体服务器,以下:
app
源码和转封装例程大部分相同,能够认为是转封装例程的加强版:
#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.flv int 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)(duration*AV_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; }
收流功能与打开普通文件代码没有区别,打开输入时,FFmpeg能识别流协议及封装格式,根据相应的协议层代码来接收流,收到流数据去掉协议层后获得的数据和普通文件内容是同样的一,后续的处理流程也就同样了。
推流有两个须要注意的地方。
一是须要根据输出流协议显式指定输出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)(duration*AV_TIME_BASE)); }
FFmpeg默认并不支持rtmp协议。须要先编译安装第三方库librtmp,而后开启--enable-librtmp选项从新编译安装FFmpeg。具体方法参考:“FFmpeg开发环境构建”
测试收流与推流功能须要搭建流媒体服务器。咱们选用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时须要添加sudo:将当前用户添加到docker组,若docker组不存在则建立
sudo gpasswd -a think docker
[2] 配置镜像加速
docker镜像源位于美国,摘取镜像很是缓慢。配置docker中国官方镜像加速registry.docker-cn.com,可加快镜像拉取速度。中国官方加速镜像只包含流行的公有镜像,私有镜像仍须要从美国镜像库中拉取。
修改 /etc/docker/daemon.json 文件并添加上 registry-mirrors 键值:
{ "registry-mirrors": ["https://registry.docker-cn.com"] }
修改上述配置文件后重启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端口添加例外,修改/etc/sysconfig/SuSEfirewall2文件,在FW_SERVICES_EXT_TCP项中添加1935端口,以下:
FW_SERVICES_EXT_TCP="ssh 1935"
而后重启防火墙:
systemctl restart SuSEfirewall2
[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流媒体服务器搭建成功,可正常使用。
在SHELL中运行以下命令下载例程源码:
svn checkout https://github.com/leichn/exercises/trunk/source/ffmpeg/ffmpeg_stream
在源码目录执行./compile.sh
命令,生成streamer可执行文件。
测试文件下载(右键另存为):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可否正常播放。
推流的问题:不论是用ffmpeg命令,仍是用本测试程序,推流结束时会打印以下信息:
[flv @ 0x22ab9c0] Timestamps are unset in a packet for stream 0. This is deprecated and will stop working in the future. Fix your code to set the timestamps properly Larger timestamp than 24-bit: 0xffffffc2 [flv @ 0x22ab9c0] Failed to update header with correct duration. [flv @ 0x22ab9c0] Failed to update header with correct filesize.
收流的问题:推流结束后,收流超时未收以数据,会打印以下信息后程序退出运行
RTMP_ReadPacket, failed to read RTMP packet header
[1] 雷霄骅, RTMP流媒体技术零基础学习方法
[2] 观止云, 【流媒体|从入门到出家】:流媒体原理(上)
[3] 观止云, 【流媒体|从入门到出家】:流媒体原理(下)
[4] 观止云, 【流媒体|从入门到出家】:流媒体系统(上)
[5] 观止云, 【流媒体|从入门到出家】:流媒体系统(下)
[6] 观止云, 总结:从一个直播APP看流媒体系统的应用
2019-03-29 V1.0 初稿