系列文章:
上一篇博客介绍了怎样用ffmpeg去播放视频.
里面用于打开视频流的avformat_open_input函数除了打开本地视频之外,实际上也能打开rtmp协议的远程视频,实现拉流:
./demo -p 本地视频路径
./demo -p rtmp://服务器ip/视频流路径
这篇文章我们来讲下怎样实现推流,然后和之前的demo代码配合就能完成推流、拉流的整个过程,实现直播。
整个直播的功能分成下面三个模块:

从上图我们可以看到rtmp是需要服务器做转发的,我们选用开源的srs.直接从github上把它的源码拉下来编译,然后直接启动即可:
git clone git@github.com:ossrs/srs.git
cd srs/trunk
./configure
make
./etc/init.d/srs start
如果是本地的电脑,这个时候就能在局域网内直接用它的内网ip去访问了.但如果是腾讯云、阿里云之类的云服务器还需要配置安全组开放下面几个端口的访问权限:
listen 1935;
max_connections 1000;
#srs_log_tank file;
#srs_log_file ./objs/srs.log;
daemon on;
http_api {
enabled on;
listen 1985;
}
http_server {
enabled on;
listen 8080;
dir ./objs/nginx/html;
}
rtc_server {
enabled on;
listen 8000; # UDP port
# @see https://ossrs.net/lts/zh-cn/docs/v4/doc/webrtc#config-candidate
candidate $CANDIDATE;
}
...
当然如果这几个端口已经被占用的话可以修改配置文件conf/srs.conf去修改
服务器到这里就准备好了,浏览器访问下面网址对srs进行调试、配置:
http://服务器ip:8080/players/rtc_publisher.html
http://服务器ip:1985/console/ng_index.html
我们选择推送本地的视频到rtmp服务器,所以第一步仍然是打开本地视频流:
bool VideoSender::Send(const string& srcUrl, const string& destUrl) {
...
// 打开文件流读取文件头解析出视频信息如轨道信息、时长等
// mFormatContext初始化为NULL,如果打开成功,它会被设置成非NULL的值
// 这个方法实际可以打开多种来源的数据,url可以是本地路径、rtmp地址等
// 在不需要的时候通过avformat_close_input关闭文件流
if(avformat_open_input(&inputFormatContext, srcUrl.c_str(), NULL, NULL) < 0) {
cout << "open " << srcUrl << " failed" << endl;
break;
}
// 对于没有文件头的格式如MPEG或者H264裸流等,可以通过这个函数解析前几帧得到视频的信息
if(avformat_find_stream_info(inputFormatContext, NULL) < 0) {
cout << "can't find stream info in " << srcUrl << endl;
break;
}
// 打印输入视频信息
av_dump_format(inputFormatContext, 0, srcUrl.c_str(), 0);
...
}
本地视频打开之后,我们创建输出视频流上下文,然后为输出流创建轨道,最后打开输出视频流:
// 创建输出流上下文,outputFormatContext初始化为NULL,如果打开成功,它会被设置成非NULL的值,在不需要的时候使用avformat_free_context释放
// 输出流使用flv格式
if(avformat_alloc_output_context2(&outputFormatContext, NULL, "flv", destUrl.c_str()) < 0) {
cout << "can't alloc output context for " << destUrl << endl;
break;
}
// 拷贝编解码参数
if(!createOutputStreams(inputFormatContext, outputFormatContext)) {
break;
}
// 打印输出视频信息
av_dump_format(outputFormatContext, 0, destUrl.c_str(), 1);
// 打开输出流,结束的时候使用avio_close关闭
if(avio_open(&outputFormatContext->pb, destUrl.c_str(), AVIO_FLAG_WRITE) < 0) {
cout << "can't open avio " << destUrl << endl;
break;
}
这里有个createOutputStreams用于根据本地视频文件的轨道信息,为输出流创建同样的轨道:
static bool createOutputStreams(AVFormatContext* inputFormatContext, AVFormatContext* outputFormatContext) {
// 遍历输入流的所有轨道,拷贝编解码参数到输出流
for(int i = 0 ; i < inputFormatContext->nb_streams ; i++) {
// 为输出流创建轨道
AVStream* stream = avformat_new_stream(outputFormatContext, NULL);
if(NULL == stream) {
cout << "can't create stream, index " << i << endl;
return false;
}
// 编解码参数在AVCodecParameters中保存,从输入流拷贝到输出流
if(avcodec_parameters_copy(stream->codecpar, inputFormatContext->streams[i]->codecpar) < 0) {
cout << "can't copy codec paramters, stream index " << i << endl;
return false;
}
// codec_tag代表了音视频数据采用的码流格式,不同的封装格式如flv、mp4等的支持情况是不一样的
// 上面的avcodec_parameters_copy将输出流的codec_tag从输入拷贝过来变成了一样的
// 由于我们输出流在avformat_alloc_output_context2的时候写死了flv格式
// 如果输入流不是flv而是mp4等格式的话就可能会出现mp4里某种codec_tag在flv不支持导致推流失败的情况
// 这里我们可以用av_codec_get_id从输出流的oformat的支持的codec_tag列表里面查找codec_id
// 如果和codecpar的codec_id不一致的话代表不支持
if(av_codec_get_id(outputFormatContext->oformat->codec_tag, stream->codecpar->codec_tag) != stream->codecpar->codec_id) {
// 这里将codec_tag设置为0,FFmpeg会根据编码codec_id从封装格式的codec_tag列表中找到一个codec_tag
stream->codecpar->codec_tag = 0;
}
}
return true;
}
这里可以看到对于编码器有codec_id和codec_tag两个字段去描述,codec_id代表的是数据的编码类型.而codec_tag用于更详细的描述编解码的格式信息,它对应的是FourCC(Four-Character Codes)数据。
例如codec_id都是AV_CODEC_ID_RAWVIDEO的裸数据,但它可能是YUV的裸数据也可能是RGB的裸数据:
// libavformat/isom.c
{ AV_CODEC_ID_RAWVIDEO, MKTAG('r', 'a', 'w', ' ') }, /* uncompressed RGB */
{ AV_CODEC_ID_RAWVIDEO, MKTAG('y', 'u', 'v', '2') }, /* uncompressed YUV422 */
{ AV_CODEC_ID_RAWVIDEO, MKTAG('2', 'v', 'u', 'y') }, /* uncompressed 8-bit 4:2:2 */
{ AV_CODEC_ID_RAWVIDEO, MKTAG('y', 'u', 'v', 's') }, /* same as 2VUY but byte-swapped */
又例如codec_id都是AV_CODEC_ID_H264,但实际上也有许多细分类型:
// libavformat/isom.c
{ AV_CODEC_ID_H264, MKTAG('a', 'v', 'c', '1') }, /* AVC-1/H.264 */
{ AV_CODEC_ID_H264, MKTAG('a', 'v', 'c', '2') },
{ AV_CODEC_ID_H264, MKTAG('a', 'v', 'c', '3') },
{ AV_CODEC_ID_H264, MKTAG('a', 'v', 'c', '4') },
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '5', 'p') }, /* AVC-Intra 50M 720p24/30/60 */
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '5', 'q') }, /* AVC-Intra 50M 720p25/50 */
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '5', '2') }, /* AVC-Intra 50M 1080p25/50 */
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '5', '3') }, /* AVC-Intra 50M 1080p24/30/60 */
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '5', '5') }, /* AVC-Intra 50M 1080i50 */
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '5', '6') }, /* AVC-Intra 50M 1080i60 */
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '1', 'p') }, /* AVC-Intra 100M 720p24/30/60 */
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '1', 'q') }, /* AVC-Intra 100M 720p25/50 */
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '1', '2') }, /* AVC-Intra 100M 1080p25/50 */
可以看出来codec_tag是通过4个字母去表示的,我们来看看MKTAG的定义:
#define MKTAG(a,b,c,d) ((a) | ((b) << 8) | ((c) << 16) | ((unsigned)(d) << 24))
最终它得到的是一个整数,例如MKTAG('a', 'v', 'c', '1')得到的值是0x31637661
我们可以用av_fourcc2str这个函数将最终的整数转换回字符串
回过头来看看这个判断:
if(av_codec_get_id(outputFormatContext->oformat->codec_tag, stream->codecpar->codec_tag) != stream->codecpar->codec_id)
大部分情况下如果codec_tag在输出流不支持的情况下av_codec_get_id拿到的是AV_CODEC_ID_NONE,所以大部分情况可以等价于:
if(av_codec_get_id(outputFormatContext->oformat->codec_tag, stream->codecpar->codec_tag) != AV_CODEC_ID_NONE)
不过也存在都是MKTAG('l', 'p', 'c', 'm'),但codec_id可能是AV_CODEC_ID_PCM_S16BE或者AV_CODEC_ID_PCM_S16LE的情况:
{ AV_CODEC_ID_PCM_S16BE, MKTAG('l', 'p', 'c', 'm') },
{ AV_CODEC_ID_PCM_S16LE, MKTAG('l', 'p', 'c', 'm') },
所以最好还是和原本的codec_id做比较会靠谱点。
接着就是视频数据的写入了,主要有三个步骤,写入文件头、读取本地视频包并写入输出视频流、写入文件结尾:
// 设置flvflags为no_duration_filesize用于解决下面的报错
// [flv @ 0x14f808e00] Failed to update header with correct duration.
// [flv @ 0x14f808e00] Failed to update header with correct filesize
AVDictionary * opts = NULL;
av_dict_set(&opts, "flvflags", "no_duration_filesize", 0);
if(avformat_write_header(outputFormatContext, opts ? &opts : NULL) < 0) {
cout << "write header to " << destUrl << " failed" << endl;
break;
}
// 创建创建AVPacket接收数据包
// 无论是压缩的音频流还是压缩的视频流,都是由一个个数据包组成的
// 解码的过程实际就是从文件流中读取一个个数据包传给解码器去解码
// 对于视频,它通常应包含一个压缩帧
// 对于音频,它可能是一段压缩音频、包含多个压缩帧
// 在不需要的时候可以通过av_packet_free释放
packet = av_packet_alloc();
if(NULL == packet) {
cout << "can't alloc packet" << endl;
break;
}
...
// 从文件流里面读取出数据包,这里的数据包是编解码层的压缩数据
while(av_read_frame(inputFormatContext, packet) >= 0) {
// 我们以视频轨道为基准去同步时间
// 如果时间还没有到就添加延迟,避免向服务器推流速度过快
...
// 往输出流写入数据
av_interleaved_write_frame(outputFormatContext, packet);
// 写入成之后压缩数据包的数据就不需要了,将它释放
av_packet_unref(packet);
}
// 写入视频尾部信息
av_write_trailer(outputFormatContext);
由于av_read_frame这里读取出来的是未解码的压缩数据速度很快,如果不做控制一下子就发送完成了,会造成数据堆积在服务器上。这里我们忽略网络传输耗时,依然通过视频包的pts做一定的同步:
while(av_read_frame(inputFormatContext, packet) >= 0) {
// 我们以视频轨道为基准去同步时间
// 如果时间还没有到就添加延迟,避免向服务器推流速度过快
if(videoStreamIndex == packet->stream_index) {
if(AV_NOPTS_VALUE == packet->pts) {
// 有些视频流不带pts数据,按30fps将间隔统一成32ms
av_usleep(32000);
} else {
// 带pts数据的视频流,我们计算出每一帧应该在什么时候播放
int64_t nowTime = av_gettime() - startTime;
int64_t pts = packet->pts * 1000 * 1000 * timeBaseFloat;
if(pts > nowTime) {
av_usleep(pts - nowTime);
}
}
}
// 往输出流写入数据
av_interleaved_write_frame(outputFormatContext, packet);
// 写入成之后压缩数据包的数据就不需要了,将它释放
av_packet_unref(packet);
}
等视频流读写完成之后就是最后的资源释放收尾工作了:
if(NULL != packet) {
av_packet_free(&packet);
}
if(NULL != outputFormatContext) {
if(NULL != outputFormatContext->pb) {
avio_close(outputFormatContext->pb);
}
avformat_free_context(outputFormatContext);
}
if(NULL != inputFormatContext) {
avformat_close_input(&inputFormatContext);
}
源码和上篇博客的是同一个仓库,编译之后可以通过-s参数推流到服务器:
./demo -s video.flv rtmp://服务器ip/live/livestream
推流的同时就能使用-p参数去拉流进行实时播放:
./demo -p rtmp://服务器ip/live/livestream
这个demo只是简单的将本地视频文件推到服务器,实际上我们可以对他做些修改就能实现将摄像头的视频流推到服务器了。
目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称
@作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors 1、什么是behaviors 2、behaviors的工作方式 3、创建behavior 4、导入并使用behavior 5、behavior中所有可用的节点 6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors 1、什么是behaviorsbehaviors是小程序中,用于实现
2022/8/4更新支持加入水印水印必须包含透明图像,并且水印图像大小要等于原图像的大小pythonconvert_image_to_video.py-f30-mwatermark.pngim_dirout.mkv2022/6/21更新让命令行参数更加易用新的命令行使用方法pythonconvert_image_to_video.py-f30im_dirout.mkvFFMPEG命令行转换一组JPG图像到视频时,是将这组图像视为MJPG流。我需要转换一组PNG图像到视频,FFMPEG就不认了。pyav内置了ffmpeg库,不需要系统带有ffmpeg工具因此我使用ffmpeg的python包装p
遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg
ES一、简介1、ElasticStackES技术栈:ElasticSearch:存数据+搜索;QL;Kibana:Web可视化平台,分析。LogStash:日志收集,Log4j:产生日志;log.info(xxx)。。。。使用场景:metrics:指标监控…2、基本概念Index(索引)动词:保存(插入)名词:类似MySQL数据库,给数据Type(类型)已废弃,以前类似MySQL的表现在用索引对数据分类Document(文档)真正要保存的一个JSON数据{name:"tcx"}二、入门实战{"name":"DESKTOP-1TSVGKG","cluster_name":"elasticsear
我正在尝试使用以下代码通过将ffmpeg实用程序作为子进程运行并获取其输出并解析它来确定视频分辨率:IO.popen'ffmpeg-i'+path_to_filedo|ffmpegIO|#myparsegoeshereend...但是ffmpeg输出仍然连接到标准输出并且ffmepgIO.readlines是空的。ffmpeg实用程序是否需要一些特殊处理?或者还有其他方法可以获得ffmpeg输出吗?我在WinXP和FedoraLinux下测试了这段代码-结果是一样的。 最佳答案 要跟进mouviciel的评论,您需要使用类似pope
文章目录1.任务背景2.任务目标3.相关知识点4.任务实操4.1安装配置JDK4.2启动FISCOBCOS4.3下载解压WeBASE-Front4.4拷贝sdk证书文件4.5启动节点4.6访问节点4.7检查运行状态5.任务总结1.任务背景FISCOBCOS其实是有控制台管理工具,用来对区块链系统进行各种管理操作。但是对于初学者来说,还是可视化界面更友好,本节就来介绍WeBASE管理平台,这是一款微众银行开源的自研区块链中间件平台,可以降低区块链使用的门槛,大幅提高区块链应用的开发效率。微众银行是腾讯牵头设立的民营银行,在国内民营银行里还是比较出名的。微众银行参与FISCOBCOS生态建设,一定
TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是
文章目录一、项目场景二、基本模块原理与调试方法分析——信源部分:三、信号处理部分和显示部分:四、基本的通信链路搭建:四、特殊模块:interpretedMATLABfunction:五、总结和坑点提醒一、项目场景 最近一个任务是使用simulink搭建一个MIMO串扰消除的链路,并用实际收到的数据进行测试,在搭建的过程中也遇到了不少的问题(当然这比vivado里面的debug好不知道多少倍)。准备趁着这个机会,先以一个很基本的通信链路对simulink基础和相关的debug方法进行总结。 在本篇中,主要记录simulink的基本原理和基本的SISO通信传输链路(QPSK方式),计划在下篇记
目录一、ESP32简单介绍二、ESP32Wi-Fi模块介绍三、ESP32Wi-Fi编程模型四、ESP32Wi-Fi事件处理流程 五、ESP32Wi-Fi开发环境六、ESP32Wi-Fi具体代码七、ESP32Wi-Fi代码解读6.1主程序app_main7.2自定义代码wifi_init_sta()八、ESP32Wi-Fi连接验证8.1测试方法8.2服务器模拟工具sscom58.3测试代码8.4测试结果前言为了开发一款亚马逊物联网产品,开始入手ESP32模块。为了能够记录自己的学习过程,特记录如下操作过程。一、ESP32简单介绍ESP32是一套Wi-Fi(2.4GHz)和蓝牙(4.2)双模解决方