本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10623968.html

1. 简介

流媒体是使用了流式传输的多媒体应用技术。如下是维基百科关于流媒体概念的定义:

流媒体 (streaming media) 是指将一连串的媒体数据压缩后,经过网络分段发送数据,在网络上即时传输影音以供观赏的一种技术与过程,此技术使得数据包得以像流水一样发送;如果不使用此技术,就必须在使用前下载整个媒体文件。

关于流媒体的基础概念,观止云的 “流媒体|从入门到出家” 系列文章极具参考价值,请参考本文第 5 节参考资料部分。

1.1 FFmpeg 影音处理的层次

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

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

容器层:处理各种封装格式。容器层由 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 收流与推流

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

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

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

2. 源码

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

  1. #include <stdbool.h>
  2. #include <libavutil/timestamp.h>
  3. #include <libavformat/avformat.h>
  4. // ffmpeg -re -i tnhaoxc.flv -c copy -f flv rtmp://192.168.0.104/live
  5. // ffmpeg -i rtmp://192.168.0.104/live -c copy tnlinyrx.flv
  6. // ./streamer tnhaoxc.flv rtmp://192.168.0.104/live
  7. // ./streamer rtmp://192.168.0.104/live tnhaoxc.flv
  8. int main(int argc, char **argv)
  9. {
  10. AVOutputFormat *ofmt = NULL;
  11. AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
  12. AVPacket pkt;
  13. const char *in_filename, *out_filename;
  14. int ret, i;
  15. int stream_index = 0;
  16. int *stream_mapping = NULL;
  17. int stream_mapping_size = 0;
  18. if (argc < 3) {
  19. printf("usage: %s input output\n"
  20. "API example program to remux a media file with libavformat and libavcodec.\n"
  21. "The output format is guessed according to the file extension.\n"
  22. "\n", argv[0]);
  23. return 1;
  24. }
  25. in_filename = argv[1];
  26. out_filename = argv[2];
  27. // 1. 打开输入
  28. // 1.1 读取文件头,获取封装格式相关信息
  29. if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) {
  30. printf("Could not open input file '%s'", in_filename);
  31. goto end;
  32. }
  33. // 1.2 解码一段数据,获取流相关信息
  34. if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
  35. printf("Failed to retrieve input stream information");
  36. goto end;
  37. }
  38. av_dump_format(ifmt_ctx, 0, in_filename, 0);
  39. // 2. 打开输出
  40. // 2.1 分配输出ctx
  41. bool push_stream = false;
  42. char *ofmt_name = NULL;
  43. if (strstr(out_filename, "rtmp://") != NULL) {
  44. push_stream = true;
  45. ofmt_name = "flv";
  46. }
  47. else if (strstr(out_filename, "udp://") != NULL) {
  48. push_stream = true;
  49. ofmt_name = "mpegts";
  50. }
  51. else {
  52. push_stream = false;
  53. ofmt_name = NULL;
  54. }
  55. avformat_alloc_output_context2(&ofmt_ctx, NULL, ofmt_name, out_filename);
  56. if (!ofmt_ctx) {
  57. printf("Could not create output context\n");
  58. ret = AVERROR_UNKNOWN;
  59. goto end;
  60. }
  61. stream_mapping_size = ifmt_ctx->nb_streams;
  62. stream_mapping = av_mallocz_array(stream_mapping_size, sizeof(*stream_mapping));
  63. if (!stream_mapping) {
  64. ret = AVERROR(ENOMEM);
  65. goto end;
  66. }
  67. ofmt = ofmt_ctx->oformat;
  68. AVRational frame_rate;
  69. double duration;
  70. for (i = 0; i < ifmt_ctx->nb_streams; i++) {
  71. AVStream *out_stream;
  72. AVStream *in_stream = ifmt_ctx->streams[i];
  73. AVCodecParameters *in_codecpar = in_stream->codecpar;
  74. if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
  75. in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
  76. in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
  77. stream_mapping[i] = -1;
  78. continue;
  79. }
  80. if (push_stream && (in_codecpar->codec_type == AVMEDIA_TYPE_VIDEO)) {
  81. frame_rate = av_guess_frame_rate(ifmt_ctx, in_stream, NULL);
  82. duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
  83. }
  84. stream_mapping[i] = stream_index++;
  85. // 2.2 将一个新流(out_stream)添加到输出文件(ofmt_ctx)
  86. out_stream = avformat_new_stream(ofmt_ctx, NULL);
  87. if (!out_stream) {
  88. printf("Failed allocating output stream\n");
  89. ret = AVERROR_UNKNOWN;
  90. goto end;
  91. }
  92. // 2.3 将当前输入流中的参数拷贝到输出流中
  93. ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
  94. if (ret < 0) {
  95. printf("Failed to copy codec parameters\n");
  96. goto end;
  97. }
  98. out_stream->codecpar->codec_tag = 0;
  99. }
  100. av_dump_format(ofmt_ctx, 0, out_filename, 1);
  101. if (!(ofmt->flags & AVFMT_NOFILE)) { // TODO: 研究AVFMT_NOFILE标志
  102. // 2.4 创建并初始化一个AVIOContext,用以访问URL(out_filename)指定的资源
  103. ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
  104. if (ret < 0) {
  105. printf("Could not open output file '%s'", out_filename);
  106. goto end;
  107. }
  108. }
  109. // 3. 数据处理
  110. // 3.1 写输出文件头
  111. ret = avformat_write_header(ofmt_ctx, NULL);
  112. if (ret < 0) {
  113. printf("Error occurred when opening output file\n");
  114. goto end;
  115. }
  116. while (1) {
  117. AVStream *in_stream, *out_stream;
  118. // 3.2 从输出流读取一个packet
  119. ret = av_read_frame(ifmt_ctx, &pkt);
  120. if (ret < 0) {
  121. break;
  122. }
  123. in_stream = ifmt_ctx->streams[pkt.stream_index];
  124. if (pkt.stream_index >= stream_mapping_size ||
  125. stream_mapping[pkt.stream_index] < 0) {
  126. av_packet_unref(&pkt);
  127. continue;
  128. }
  129. int codec_type = in_stream->codecpar->codec_type;
  130. if (push_stream && (codec_type == AVMEDIA_TYPE_VIDEO)) {
  131. av_usleep((int64_t)(duration*AV_TIME_BASE));
  132. }
  133. pkt.stream_index = stream_mapping[pkt.stream_index];
  134. out_stream = ofmt_ctx->streams[pkt.stream_index];
  135. /* copy packet */
  136. // 3.3 更新packet中的pts和dts
  137. // 关于AVStream.time_base(容器中的time_base)的说明:
  138. // 输入:输入流中含有time_base,在avformat_find_stream_info()中可取到每个流中的time_base
  139. // 输出:avformat_write_header()会根据输出的封装格式确定每个流的time_base并写入文件中
  140. // AVPacket.pts和AVPacket.dts的单位是AVStream.time_base,不同的封装格式AVStream.time_base不同
  141. // 所以输出文件中,每个packet需要根据输出封装格式重新计算pts和dts
  142. av_packet_rescale_ts(&pkt, in_stream->time_base, out_stream->time_base);
  143. pkt.pos = -1;
  144. // 3.4 将packet写入输出
  145. ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
  146. if (ret < 0) {
  147. printf("Error muxing packet\n");
  148. break;
  149. }
  150. av_packet_unref(&pkt);
  151. }
  152. // 3.5 写输出文件尾
  153. av_write_trailer(ofmt_ctx);
  154. end:
  155. avformat_close_input(&ifmt_ctx);
  156. /* close output */
  157. if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE)) {
  158. avio_closep(&ofmt_ctx->pb);
  159. }
  160. avformat_free_context(ofmt_ctx);
  161. av_freep(&stream_mapping);
  162. if (ret < 0 && ret != AVERROR_EOF) {
  163. printf("Error occurred: %s\n", av_err2str(ret));
  164. return 1;
  165. }
  166. return 0;
  167. }

2.1 收流

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

2.2 推流

推流有两个需要注意的地方。

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

  1. bool push_stream = false;
  2. char *ofmt_name = NULL;
  3. if (strstr(out_filename, "rtmp://") != NULL) {
  4. push_stream = true;
  5. ofmt_name = "flv";
  6. }
  7. else if (strstr(out_filename, "udp://") != NULL) {
  8. push_stream = true;
  9. ofmt_name = "mpegts";
  10. }
  11. else {
  12. push_stream = false;
  13. ofmt_name = NULL;
  14. }
  15. avformat_alloc_output_context2(&ofmt_ctx, NULL, ofmt_name, out_filename);

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

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

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

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

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

  1. int codec_type = in_stream->codecpar->codec_type;
  2. if (push_stream && (codec_type == AVMEDIA_TYPE_VIDEO)) {
  3. av_usleep((int64_t)(duration*AV_TIME_BASE));
  4. }

3. 验证

3.1 编译第三方库 librtmp

FFmpeg 默认并不支持 rtmp 协议。需要先编译安装第三方库 librtmp,然后开启 --enable-librtmp 选项重新编译安装 FFmpeg。具体方法参考:"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:

  1. sudo zypper install docker

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

  1. sudo gpasswd -a ${USER} docker

[2] 配置镜像加速

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

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

  1. {
  2. "registry-mirrors":
  3. [
  4. "https://registry.docker-cn.com",
  5. "https://docker.mirrors.ustc.edu.cn",
  6. "https://hub-mirror.c.163.com",
  7. "https://mirror.ccs.tencentyun.com"
  8. ]
  9. }

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

修改配置文件后重启 docker 服务:

  1. systemctl restart docker

[3] 拉取 nginx-rtmp 镜像

  1. docker pull tiangolo/nginx-rtmp

[4] 打开容器

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

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

如果无法推流,应在防火墙中将 1935 端口添加例外,修改 /etc/sysconfig/SuSEfirewall2 文件,在 FW_SERVICES_EXT_TCP 项中添加 1935 端口,如下:

  1. FW_SERVICES_EXT_TCP="ssh 1935"

然后重启防火墙:

  1. systemctl restart SuSEfirewall2

[6] 验证服务器

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

ffmpeg 推流测试:

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

"-re":按视频帧率的速度读取输入

"-c copy":输出流使用和输入流相同的编解码器

"-f flv":指定输出流封装格式为flv

ffplay 收流播放测试:

  1. ffplay rtmp://192.168.0.104/live

ffplay 播放正常,说明 nginx-rtmp 流媒体服务器搭建成功,可正常使用。

3.3 编译

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

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

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

3.4 验证

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

推流测试:

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

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

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

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

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

以上测试命令等价于:

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

接收结束后检查一下生成的本地文件 shifu.ts 能否正常播放。

4. 遗留问题

推流的问题:不管是用 ffmpeg 命令,还是用本测试程序,推流结束时会打印如下信息

  1. [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
  2. Larger timestamp than 24-bit: 0xffffffc2
  3. [flv @ 0x22ab9c0] Failed to update header with correct duration.
  4. [flv @ 0x22ab9c0] Failed to update header with correct filesize.

收流的问题:推流结束后,收流超时未收以数据,会打印如下信息后程序退出运行

  1. RTMP_ReadPacket, failed to read RTMP packet header

5. 参考资料

[1] 雷霄骅, RTMP流媒体技术零基础学习方法

[2] 观止云, 【流媒体|从入门到出家】:流媒体原理(上)

[3] 观止云, 【流媒体|从入门到出家】:流媒体原理(下)

[4] 观止云, 【流媒体|从入门到出家】:流媒体系统(上)

[5] 观止云, 【流媒体|从入门到出家】:流媒体系统(下)

[6] 观止云, 总结:从一个直播APP看流媒体系统的应用

6. 修改记录

2019-03-29 V1.0 初稿

FFmpeg流媒体处理-收流与推流的更多相关文章

  1. javaCV开发详解之4:转流器实现(也可作为本地收流器、推流器,新增添加图片及文字水印,视频图像帧保存),实现rtsp/rtmp/本地文件转发到rtmp流媒体服务器(基于javaCV-FFMPEG)

    javaCV系列文章: javacv开发详解之1:调用本机摄像头视频 javaCV开发详解之2:推流器实现,推本地摄像头视频到流媒体服务器以及摄像头录制视频功能实现(基于javaCV-FFMPEG.j ...

  2. javaCV开发详解之3:收流器实现,录制流媒体服务器的rtsp/rtmp视频文件(基于javaCV-FFMPEG)

    javaCV系列文章: javacv开发详解之1:调用本机摄像头视频 javaCV开发详解之2:推流器实现,推本地摄像头视频到流媒体服务器以及摄像头录制视频功能实现(基于javaCV-FFMPEG.j ...

  3. ffmpeg处理rtmp/文件/rtsp的推流和拉流

    ffmpeg处理rtmp/文件/rtsp的推流和拉流   本demo演示了利用ffmpeg从服务器拉流或本地文件读取流,更改流url或文件类型名称发送回服务器或存到本地的作用. 由于本程序只写了3个小 ...

  4. FFmpeg命令读取RTMP流如何设置超时时间

    子标题:FFmpeg命令录制RTMP流为FLV文件时如何设置超时时间 | FFmpeg命令如何解决录制产生阻塞的问题0x001: 前言 今天在测试程序时遇到两个问题.Q1:ffmpeg录制RTMP流并 ...

  5. vlc命令行: 转码 流化 推流

    vlc命令行: 转码 流化 推流 写在命令行之前的话: VLC不仅仅可以通过界面进行播放,转码,流化,也可以通过命令行进行播放,转码和流化.还可以利用里面的SDK进行二次开发. vlc命令行使用方法: ...

  6. 流媒体测试笔记记录之————阿里云监控、OBS、FFmpeg拉流和推流变化比较记录

    OBS设置视频(512kbps)和音频(128kbps)比特率 阿里云监控结果: 使用FFmpeg拉流到Nginx 服务器测试比特率 第二次测试,修改视频和音频比特率 OBS设置 阿里云监控 Ngin ...

  7. centos7+nginx+rtmp+ffmpeg搭建流媒体服务器(保存流目录与http目录不要随意配置,否则有权限问题)

    搭建nginx-http-flv-module升级代替rtmp模块,详情:https://github.com/winshining/nginx-http-flv-module/blob/master ...

  8. ffmpeg接收udp输入的h264文件流,推流到rtmp服务器

    ffmpeg -re -f h264 -i udp://192.168.5.49:10002 -vcodec libx264 -f flv rtmp://192.168.5.155/live/1

  9. ffmpeg推送直播流的技术进展

    首先安装好NGINX并打开服务 然后安装好ffmpeg 然后参考:http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=2879051 ...

随机推荐

  1. Chapter3_操作符_直接常量和指数计数法

    (1)直接常量 在程序中使用直接常量,相当于指导编译器,告诉它要生成什么样的类型,这样就不会产生模棱两可的情况.比如flaot a = 1f等,后缀表示告诉编译器想生成的类型.常用的后缀有l/L(lo ...

  2. php.ini 配置详解

    这个文件必须命名为''php.ini''并放置在httpd.conf中的PHPIniDir指令指定的目录中.最新版本的php.ini可以在下面两个位置查看:http://cvs.php.net/vie ...

  3. kbmmw 5.07 正式发布

    来了来了 5.07.00 Dec 9 2018           Important notes (changes that may break existing code)         === ...

  4. A股、B股区别

    A股也称为人民币普通股票.流通股.社会公众股.普通股.是指那些在中国大陆注册.在中国大陆上市的普通股票.以人民币认购和交易. A股不是实物股票,以无纸化电子记帐,实行“T+1”交割制度,有涨跌幅(10 ...

  5. javascript跨域传递消息 / 服务器实时推送总结

    参考文档,下面有转载[非常好的两篇文章]: http://www.cnblogs.com/loveis715/p/4592246.html [跨源的各种方法总结] http://kb.cnblogs. ...

  6. using python read/write HBase data

    A. operations on Server side 1. ensure hadoop and hbase are working properly 2. install thrift:  apt ...

  7. android资源文件

    代码与资源分离原则:便于维护与修改shape:定义图形 selector:按照不同的情况加载不同的color或drawable layer-list:从下往上图形层叠加载 资源文件有:/res/dra ...

  8. [ 10.03 ]CF每日一题系列—— 534B贪心

    Descripe: 贪心,贪在哪里呢…… 给你初始速度,结尾速度,行驶秒数,每秒速度可变化的范围,问你行驶秒数内最远可以行驶多少距离 Solution: 贪心,我是否加速,就是看剩下的时间能不能减到原 ...

  9. samba服务配置(一)

    samba是一个实现不同操作系统之间文件共享和打印机共享的一种SMB协议的免费软件. samba软件结构: /etc/samba/smb.conf    #samba服务的主要配置文件 /etc/sa ...

  10. [转]kaldi中的特征提取

    转:http://blog.csdn.net/wbgxx333/article/details/25778483 本翻译原文http://kaldi.sourceforge.net/feat.html ...