sudo ./objs/srs -c conf/rtmp.conf
./RTMP_DEMO
注意:设置客户端请求的地址要和 SRS 服务启动的地址一样。
eg:客户端中设置的是:
#define RTMP_URL "rtmp://192.168.1.3/live/livestream"
查看客户端的 log,发现连接成功:
打印 SRS 服务端的 log:发现 RTMP Client 连接成功,因为这个客户端也是在本机上启动的,所以地址也是 198.168.1.3。
RTMP 客户端进行推流,下面简单说下实现原理。
初始化 RTMP推流对象:RTMPPusher
解析 RTMP URL,eg :“rtmp://192.168.1.3/live/livestream”
(1) 对url做合法性的校验,不合法直接返回;
(2) 解析url,解析出:protocol、host、path、port。
protocol:16
host:192.168.1.3/live/livestream
path:live/livestream
port:1935
RTMPPusher发起请求Connect
(1) 构建 Socket
struct sockaddr_in service;
memset(&service, 0, sizeof(struct sockaddr_in));
service.sin_family = AF_INET; // 指定(TCP/IP – IPv4)
service->sin_addr.s_addr = inet_addr(hostname) //指定目的ip地址:192.168.1.3
service->sin_port = htons(port); //指定目的端口号port:1935
(2) 建立TCP连接
/*1、创建 Socket */
r->m_sb.sb_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
/*2、开始3次握手,尝试建立 TCP 连接*/
connect(r->m_sb.sb_socket, service, sizeof(struct sockaddr));
/*3、成功建立连接*/
LogInfo("TCP Connect Success!!!");
(3) 建立 RTMP Connection
int ret;
/* 1、RTMP Hand Shake*/
ret = HandShake(r, TRUE)
if(ret == 0){
RTMP_LogInfo(RTMP_LOGERROR, "%s, RTMP handshake failed.", __FUNCTION__);
RTMP_Close(r);
return FALSE;
}
RTMP_LogInfo(RTMP_LOGINFO, "%s, RTMP handshaked success", __FUNCTION__);
/*2、发送 connect 命令消息(Command Message)
* 如果熟悉RTMP协议的话,就知道在RTMP握手完成之后,
* 就要发送⼀个connect命令消息,⽤于客户端向服务器发送连接请求,
* 如果服务端同意连接,会返回连接成功的消息。
*/
ret = SendConnectPacket(r,cp);
if(ret==0){
RTMP_LogInfo(RTMP_LOGERROR, "%s,Setp 3 RTMP connect failed.", __FUNCTION__);
RTMP_Close(r);
return FALSE;
}
/*3、到这里 RTMP Connection 成功建立*/
RTMP_LogInfo(RTMP_LOGINFO, "Setp 3 connect. msg cmd (typeID=20) (Connect) ok");
return TRUE;
HandShake、SendConnectPacket都是 librtmp 里面函数,需要对 RTMP 协议熟悉才能看懂。
RTMP协议是应⽤层协议,是要靠底层可靠的传输层协议(通常是TCP)来保证信息传输的可靠性的。
在传输层TCP协议的连接建⽴完成后,RTMP协议也要客户端和服务器通过“握⼿”来建⽴基于传输层
链接之上的RTMP Connection链接。
在握手完成后,先通过发送 connect 命令消息,来建立 NetConnection,才能进行会话。
要建⽴⼀个有效的RTMP Connection链接,⾸先要“握⼿”:客户端要向服务器发送C0,C1,C2(按序)三个 chunk,
服务器向客户端发送S0,S1,S2(按序)三个chunk,然后才能进⾏有效的信息传输。
本身并没有规定这6个Message的具体传输顺序,但RTMP协议的实现者需要保证这⼏点:
1、客户端要等收到S1之后才能发送C2;
2、客户端要等收到S2之后才能发送其他信息(控制信息和真实⾳视频等数据);
3、服务端要等到收到C0之后发送S1;
4、服务端必须等到收到C1之后才能发送S2;
5、服务端必须等到收到C2之后才能发送其他信息(控制信息和真实⾳视频等数据)
(4) 建立 RTMP Stream
通过发送 Create Stream 命令消息,来建立 NetStream 信息通道 。
Netstream建⽴在NetConnection(第 3 步)之上,通过NetConnection的createStream命令创建,⽤于传输具体的⾳频、视频等信息。在传输层协议之上只能连接⼀个NetConnection,但⼀个NetConnection可以建⽴ 多个NetStream来建⽴不同的流通道传输数据。
当发送完CreateStream消息后,解析服务器返回的消息会得到⼀个stream ID, 这个ID也就是以后和服 务器通信的 message stream ID, ⼀般返回的是1,不固定。
if (!RTMP_ConnectStream(rtmp_, 0))
{
LogInfo("RTMP_ConnectStream failed");
return FALSE;
}
(5)音频编码、音频重采样、视频编码器初始化的过程
// 初始化publish (推流起始时间 )
AVPublishTime::GetInstance()->Rest();
// 设置音频编码器,并初始化
audio_encoder_ = new AACEncoder();
Properties aud_codec_properties;
aud_codec_properties.SetProperty("sample_rate", audio_sample_rate_);
aud_codec_properties.SetProperty("channels", audio_channels_);
aud_codec_properties.SetProperty("bitrate", audio_bitrate_);
if(audio_encoder_->Init(aud_codec_properties) != RET_OK)
{
LogError("AACEncoder Init failed");
return RET_FAIL;
}
aac_fp_ = fopen("push_dump.aac", "wb");
if(!aac_fp_)
{
LogError("fopen push_dump.aac failed");
return RET_FAIL;
}
//设置音频重采样:将 s16 交错模式的 PCM 数据 ----> AV_SAMPLE_FMT_FLT 棋盘格式的数据
audio_resampler_ = new AudioResampler();
AudioResampleParams aud_params;
aud_params.logtag = "[audio-resample]";
aud_params.src_sample_fmt = (AVSampleFormat)mic_sample_fmt_;
aud_params.dst_sample_fmt = (AVSampleFormat)audio_encoder_->get_sample_format();
aud_params.src_sample_rate = mic_sample_rate_;
aud_params.dst_sample_rate = audio_encoder_->get_sample_rate();
aud_params.src_channel_layout = av_get_default_channel_layout(mic_channels_);
aud_params.dst_channel_layout = audio_encoder_->get_channel_layout();
aud_params.logtag = "audio-resample-encode";
audio_resampler_->InitResampler(aud_params);
//设置视频编码器
video_encoder_ = new H264Encoder();
Properties vid_codec_properties;
vid_codec_properties.SetProperty("width", video_width_);
vid_codec_properties.SetProperty("height", video_height_);
vid_codec_properties.SetProperty("fps", video_fps_);
vid_codec_properties.SetProperty("b_frames", video_b_frames_);
vid_codec_properties.SetProperty("bitrate", video_bitrate_);
vid_codec_properties.SetProperty("gop", video_gop_);
if(video_encoder_->Init(vid_codec_properties) != RET_OK)
{
LogError("H264Encoder Init failed");
return RET_FAIL;
}
h264_fp_ = fopen("push_dump.h264", "wb");
if(!h264_fp_)
{
LogError("fopen push_dump.h264 failed");
return RET_FAIL;
}
(6)构造 FLV 格式,因为RTMP推流是以FLV的格式去发送
FLV 格式中一个重要的字段:metaData
可以从上图看出 FLV 的 metaData 字段,保存着FLV 视频和音频的元信息。
FLVMetadataMsg *metadata = new FLVMetadataMsg();
// 设置视频相关
metadata->has_video = true;
metadata->width = video_encoder_->get_width();
metadata->height = video_encoder_->get_height();
metadata->framerate = video_encoder_->get_framerate();
metadata->videodatarate = video_encoder_->get_bit_rate();
// 设置音频相关
metadata->has_audio = true;
metadata->channles = audio_encoder_->get_channels();
metadata->audiosamplerate = audio_encoder_->get_sample_rate();
metadata->audiosamplesize = 16;
metadata->audiodatarate = 64;
metadata->pts = 0;
/* metadata push到消息队列 */
rtmp_pusher->Post(RTMP_BODY_METADATA, metadata, false);
(7) 设置音频捕获器
音频捕获器的作用:
1、打开要发送的pcm文件
2、启动一个线程,循环发送pcm数据到pcm_buf
3、执行回调消费pcm_buf
4、将pcm_buf重采样后,进行编码。
代码只看关键地方:
//音频捕获器初始化,在这个函数里面会打开pcm文件,得到fd:pcm_fp_
audio_capturer_->Init(aud_cap_properties)
//Start里面会启动线程执行Loop操作
audio_capturer_->Start()
//Loop操作:会从打开的pcm文件中持续读取数据到pcm_buf中,然后传入回调callback_get_pcm_消费。
void AudioCapturer::Loop()
{
int nb_samples = 1024;
pcm_total_duration_ = 0;
pcm_start_time_ = TimesUtil::GetTimeMillisecond();
while(true)
{
if(request_exit_)
break;
/* 每次读取1024个sample到pcm_buf中*/
if(readPcmFile(pcm_buf_, nb_samples) == 0)
{
if(!is_first_frame_) {
is_first_frame_ = true;
LogInfo("%s:t%u", AVPublishTime::GetInstance()->getAInTag(),
AVPublishTime::GetInstance()->getCurrenTime());
}
if(callback_get_pcm_)
{
//执行回调,callback_get_pcm,将pcm_buf传入然后消费。
callback_get_pcm_(pcm_buf_, nb_samples *4); // 2通道 s16格式
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
closePcmFile();
}
//执行callback_get_pcm_回调
/*
* pcm:就是pc_buf
* size:4096 ==> 1024个样本 * 2通道 * 16位采样格式
*/
void PushWork::PcmCallback(uint8_t *pcm, int32_t size){
if(need_send_audio_spec_config)
{
need_send_audio_spec_config = false;
/* 生成 AudioSpecMsg 消息,推给服务端。
*/
AudioSpecMsg *aud_spc_msg = new AudioSpecMsg(audio_encoder_->get_profile(),
audio_encoder_->get_channels(),
audio_encoder_->get_sample_rate());
aud_spc_msg->pts_ = 0;
/*
* 对此大家可能很困惑?为什么要先把 Audio的配置信息(采样率、通道数...)先单独推送到服务端,
* 为什么不和下面的数据一起推过去?其实在视频数据包推送哪里也会有类似的处理,它需要先把关键帧
* 的 sps 和 pps 先单独推送到服务端。
*
* 因为:假设sps和pps作为配置信息存放在关键帧的报文中推送到服务端,如果出现网络错误导致
* 这个关键帧没有到达服务端,缺少了 sps 和 pps,那后面推送的 B 帧、P 帧也无法播放了。
*
* 针对上面问题:在推流的时候,sps 和 pps 不和关键帧放在一起往服务端推送,而是先把 sps+pps 数
* 据成功推到服务端后,才能推后面的帧。如果 sps、pps 推失败了,后面推多少都是白推。
*/
rtmp_pusher->Post(RTMP_BODY_AUD_SPEC, aud_spc_msg);
}
// 1、创建一个AVFrame,参数(sample_rate、channel_layout、nb_samples、nb_channels)
// 根据重采样后的pcm数据格式进行设置
// 2、将pcm buf中的数据填充到AVFrame的data中
// 3、取出AVFrame的data中的pcm数据,开始进行重采样
// 4、将重采样后的数据写入AVAudioFifo
auto ret = audio_resampler_->SendResampleFrame(pcm, size);
// 1、构造一个AVFrame,参数根据重采样pcm后的格式进行设置
// 2、从AVAudioFifo中取出pcm数据,填充到这个AVFrame的data中(有点困惑,上面的重采样的AVFrame其实可以共用到这里,为什么要经过一手AVAudioFifo?)
// 3、resampled_frames.push_back(frame)
vector<shared_ptr<AVFrame>> resampled_frames;
ret = audio_resampler_->ReceiveResampledFrame(
resampled_frames,
audio_encoder_->GetFrameSampleSize());
// 音频数据编码
// 1、将重采样后的pcm进行AAC编码
int aac_size = audio_encoder_->Encode(resampled_frames[i].get(),
aac_buf_, AAC_BUF_MAX_LENGTH);
}
AudioSpec是FLV Audio Tag区域的字段,当AACPacketType ==0,就代表是这个Audio Tag里面装的是音频的配置信息。
当AACPacketType ==1 就代表Audio Tag区域里面是音频数据。
(8)构造ADTS流发送到服务端
ADTS是AAC音频的传输流格式
1、很显然,下面先构造ADTS的数据,然后写到aac_buf_
2、然后构造AudioRawMsg,将aac_buf_的数据拷贝给AudioRawMsg中的data
3、发送AudioRawMsg消息。
int aac_size = audio_encoder_->Encode(resampled_frames[i].get(),
aac_buf_, AAC_BUF_MAX_LENGTH);
if(aac_size > 0)
{
if(aac_fp_)
{
uint8_t adts_header[7]; //ADTS Header占7个字节
/* 构造ADTS Header*/
audio_encoder_->GetAdtsHeader(adts_header, aac_size);
fwrite(adts_header, 1, 7, aac_fp_);
fwrite(aac_buf_, 1, aac_size, aac_fp_);
fflush(aac_fp_);
}
AudioRawMsg *aud_raw_msg = new AudioRawMsg(aac_size + 2);
// 打上时间戳
aud_raw_msg->pts = AVPublishTime::GetInstance()->get_audio_pts();
aud_raw_msg->data[0] = 0xaf;
aud_raw_msg->data[1] = 0x01; // 1 = raw data数据
memcpy(&aud_raw_msg->data[2], aac_buf_, aac_size);
rtmp_pusher->Post(RTMP_BODY_AUD_RAW, aud_raw_msg);
LogDebug("PcmCallback Post");
}
ADTS流 = ADTS Header(7字节) + Audio AAC Data(aac_size)
ADTS Header:
1、sampling_frequency_index: 比如我设置pcm采样率是48khz,那么index就是3。
2、syncword:同步头占12位,总是0xFFF
3、ID: MPEG标示符,0表示MPEG-4 , 1表示MPEG-2,我这里是1
4、Layer:总是0x00
5、profile:表示使用那个级别的AAC
6、protection_absent: 表示是否误码校验
...
(8) 设置视频捕获器
初始化
double video_frame_duration = 1000.0 / video_encoder_->get_framerate();
LogInfo("video_frame_duration:%lf", video_frame_duration);
AVPublishTime::GetInstance()->set_video_pts_strategy(AVPublishTime::PTS_RECTIFY);//帧间隔矫正
video_capturer = new VideoCapturer();
Properties vid_cap_properties;
vid_cap_properties.SetProperty("video_test", 1);
vid_cap_properties.SetProperty("input_yuv_name", input_yuv_name_);
vid_cap_properties.SetProperty("width", desktop_width_);
vid_cap_properties.SetProperty("height", desktop_height_);
if(video_capturer->Init(vid_cap_properties) != RET_OK)
{
LogError("VideoCapturer Init failed");
return RET_FAIL;
}
启动Loop线程,循环读取yuv文件
yuv_buf_size = width_ * height_ * 1.5;
yuv_buf_ = new uint8_t[yuv_buf_size];
while(true)
{
if(request_exit_)
break;
if(readYuvFile(yuv_buf_, yuv_buf_size) == 0)
{
if(!is_first_frame_) {
is_first_frame_ = true;
LogInfo("%s:t%u", AVPublishTime::GetInstance()->getVInTag(),
AVPublishTime::GetInstance()->getCurrenTime());
}
if(callable_object_)
{
callable_object_(yuv_buf_, yuv_buf_size);
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
执行回调callable_object_:YuvCallback消费数据yuv_buf
if(need_send_video_config)
{
/* 发送videoConfig Message */
need_send_video_config = false;
VideoSequenceHeaderMsg * vid_config_msg = new VideoSequenceHeaderMsg(
video_encoder_->get_sps_data(),
video_encoder_->get_sps_size(),
video_encoder_->get_pps_data(),
video_encoder_->get_pps_size()
);
vid_config_msg->nWidth = video_width_;
vid_config_msg->nHeight = video_height_;
vid_config_msg->nFrameRate = video_fps_;
vid_config_msg->nVideoDataRate = video_bitrate_;
vid_config_msg->pts_ = 0;
rtmp_pusher->Post(RTMP_BODY_VID_CONFIG, vid_config_msg);
}
FLV的Video Tag Data部分,如下:
AVCPacketType ==0 时,data部分就是Specific,我打开这段flv文件,是h264编码(codecId = 7)
可以在Specific里面看到,H264编码中关键的参数:sps、pps
所以:Specific配置部分就是NALU的头
开始对视频yuv数据进行编码,编码好的packet存放到video_nalu_buf
然后将video_nalu_buf中的packet封装成NALU,通过Video Message发送出去。
if(video_encoder_->Encode(yuv, 0, video_nalu_buf, video_nalu_size_) == 0)
{
// 获取到编码数据
NaluStruct *nalu = new NaluStruct(video_nalu_buf, video_nalu_size_);
nalu->type = video_nalu_buf[0] & 0x1f;
nalu->pts = AVPublishTime::GetInstance()->get_video_pts();
rtmp_pusher->Post(RTMP_BODY_VID_RAW, nalu);
LogDebug("YuvCallback Post");
}
本文简要介绍了RTMP推流的原理以及过程,只希望对RTMP的推流过程以及原理建立一个简单的认识而已,建议配合RTMP的协议规范和rtmpdump代码进行阅读。本文讲的比较简单,对FLV格式、ADTS、NALU、RTMP的Message机制都忽略不讲,这每一个都是长篇大论,不可能在一文中全部讲出,这也不是本文主要表达的东西。
文章浏览阅读406次。转 高频交易及量化投资的策略与误区一、高频交易公司和量化投资公司的区别一般来说,高频交易公司和量化投资公司既有联系,又有区别。在美国,人们常说的高频交易公司一般都是自营交易公司,这些公司主要有Getco、Tower Research、Hudson River Trading、SIG、Virtu Financial、Jump Trading、RGM Advisor、Chopper Tradi..._hudson river trading
文章浏览阅读865次。文件的打开和关闭文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件 的关系。ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。FILE * fopen ( const char * filename, const char * mode ); int fcl..._c语言与文件处理有关的函数
文章浏览阅读1.1k次。从来没见过进行文件读取写入时,在写入中需要随机数的,你读取文件就是从一个地方获取输入流,然后将这个输入流写到别的地方,根本不要随机数。给你一个示例://copyafiletoanotherfilebyusingFileReader/FileWriterimportjava.io.*;publicclassTFileRead{publicstaticvoidmain(S..._java复制文件文件没有内容显示
文章浏览阅读556次,点赞2次,收藏3次。由于工作上的需要,今天捣鼓了半天高德地图。如果定制化开发需求不太高的话,可以用vue-amap,这个我就不多说了,详细就看官网 https://elemefe.github.io/vue-amap/#/zh-cn/introduction/install然而我们公司需要英文版的高德,我看vue-amap中好像没有这方面的配置,而且还有一些其他的定制化开发需求,然后就只用原生的高德。其实原生的引入也不复杂,但是有几个坑要填一下。1. index.html注意,引入的高德js一定要放在头部而_前端引入原生地图
文章浏览阅读104次。本文介绍ViewGroup重写,我们所熟知的LinearLayout,RelativeLayout,FrameLayout等等,所有的容器类都是ViewGroup的子类,ViewGroup又继承View。我们在熟练应用这些现成的系统布局的时候可能有时候就不能满足我们自己的需求了,这是我们就要自己重写一个容器来实现效果。ViewGroup重写可以达到各种效果,下面写一个简单的重写一个Vi..._viewgroup 重写
文章浏览阅读1.8w次,点赞279次,收藏1.5k次。本文章主要记录本人在学习stm32过程中的笔记,也插入了不少的例程代码,方便到时候CV。绝大多数内容为本人手写,小部分来自stm32官方的中文参考手册以及网上其他文章;代码部分大多来自江科大和正点原子的例程,注释是我自己添加;配图来自江科大/正点原子/中文参考手册。笔记内容都是平时自己一点点添加,不知不觉都已经这么长了。其实每一个标题其实都可以发一篇,但是这样搞太琐碎了,所以还是就这样吧。_stm32笔记
文章浏览阅读197次。面向对象的习题课类的定义员工类Employee求和类Sum类与对象书籍类BookBook类的测试类BookTest异常能扩容的MyList类剪刀石头布转载请注明出处在这一讲中我会给出一些关于面向对象部分的习题,同样希望在不看答案的情况下自己编写,即使看过了答案,也要能够在不看答案的情况下写出来。类的定义员工类Employee定义在同一个公司工作的Employee类,要求其中含有属性:员工的名字,员工的年龄,员工的爱好,员工的公司名(注意当公司更名时,所有员工的公司名都需要更名),工作地点默认为中国(_编写一个测试类booktest,创建几个book对象,并打印它们的字符串表示,同时判断
文章浏览阅读6.7k次,点赞7次,收藏14次。一、伪分布安装Spark安装环境:Ubuntu 14.04 LTS 64位+Hadoop2.7.2+Spark2.0.0+jdk1.7.0_761、安装jdk1.7(1)下载jdk-7u76-linux-x64.tar.gz;(2)解压jdk-7u76-linux-x64.tar.gz,并将其移动到/opt/java/jdk路径下(自建);命令:tar -zxvf jdk-_下载spark的hadoop依赖
文章浏览阅读6.7k次。计算机通信协议计算机通信协议是对那些计算机必须遵守以便彼此通信的规则的描述。什么是 TCP/IP?TCP/IP 是供已连接因特网的计算机进行通信的通信协议。TCP/IP 指传输控制协议/网际协议 (Transmission Control Protocol / Internet Protocol)。TCP/IP 定义了电子设备(比如计算机)如何连入因特网,以及数据如何在它们之间传输的标准..._广泛应用在internet中的tcp/ip的网络管理主要使用的是 ____协议。 (填空题)
文章浏览阅读360次。转自:落尘曦的博客:http://blog.csdn.net/qq_23994787 原文链接:https://blog.csdn.net/qq_23994787/article/details/77951244#_Toc9101经典算法的Java实现(1)河内塔问题: 42(2)费式数列 43(3)巴斯卡(Pascal)三角形 44(4)蒙地卡罗法求 PI 45(..._java中temsize+=1运算
文章浏览阅读3.1k次,点赞6次,收藏27次。第一章Q1 简述Linux系统的应用领域 Linux服务器;嵌入式Linux系统;软件开发平台;桌面应用Q2 简述Linux系统的特点 开放性、多用户、多任务、良好的用户界面、设备独立性、丰富的网络功能、可靠的系统安全、良好的可移植性Q3 简述Linux系统的组成 内核、shell、文件系统、应用程序Q4 简述主流的Linux发行版本 Redhat SUSE Oracle CentOS Ubuntu Debian Mandriva Gentoo Slackware Fe_linux中,第一个普通用户的uid为____。
文章浏览阅读183次。粒子群算法新型概率密度无人机作战路径规划完整的代码,方可运行;可提供运行操作视频!适合小白!_已知目标出现概率热图matlab无人机路径规划