目录
前言:
一、主要流程
二、详细介绍
三、总结
目前直播领域业界流行的协议主要有三个:rtmp、http-flv、hls。其中,http-flv、hls是终端播放(称为下行)经常使用的协议,rtmp通常作为推流协议(称为上行)使用。srs主要有两种模式:回源模式 和 推流模式,笔者由于项目中主要涉及到的是回源模式,所以本文重点分析一下srs中从收到rtmp播放请求到被动去使用rtmp协议向上回源的整个流程。
建立rtmp连接---获取或创建SrsSource---创建consumer-------循环获取consumer中的msg并按rtmp协议发送(同时处理客户端信令)
| |---独立的微线程接收客户端发送的rtmp信令
|---触发SrsSource中的ingester回源
从rtmp建连说起,首先来看SrsConnection和她的子类。srs中每个客户端的连接都会对应一个SrsConnection对象,但是SrsConnection是一个虚类,类似于一个接口。具体的连接对象实际上是SrsConnection的子类对象:
SrsRtmpConn(rtmp请求)
SrsHttpApi(api请求)
SrsResponseOnlyHttpConn(http-flv和hls请求)
srs收到rtmp播放请求,会生成一个SrsRtmpConn对象,并创建一个微线程去独立处理这个请求,微线程会走到SrsConnection::cycle():
int SrsConnection::cycle()
{
...
ret = do_cycle();
...
}
cycle()主要调用了SrsRtmpConn的 do_cycle(),这个函数主要完成rtmp连接和connect命令的处理,然后把将后续处理交给SrsRtmpConn::service_cycle()去处理。另外,当service_cycle()返回,也就是播放停止时,会根据配置需要进行停播回调通知。
int SrsRtmpConn::do_cycle()
{
int ret = ERROR_SUCCESS;
srs_trace("RTMP client ip=%s", ip.c_str());
//设置tcp连接的发送和接收超时
rtmp->set_recv_timeout(SRS_CONSTS_RTMP_RECV_TIMEOUT_US);
rtmp->set_send_timeout(SRS_CONSTS_RTMP_SEND_TIMEOUT_US);
// 建立rtmp连接,支持简单连接和复杂连接,一般使用的是复杂连接
if ((ret = rtmp->handshake()) != ERROR_SUCCESS) {
srs_error("rtmp handshake failed. ret=%d", ret);
return ret;
}
srs_verbose("rtmp handshake success");
// 接收客户端的connect命令,主要获取tcUrl
if ((ret = rtmp->connect_app(req)) != ERROR_SUCCESS) {
srs_error("rtmp connect vhost/app failed. ret=%d", ret);
return ret;
}
srs_verbose("rtmp connect app success");
// set client ip to request.
req->ip = ip;
srs_trace("connect app, "
"tcUrl=%s, pageUrl=%s, swfUrl=%s, schema=%s, vhost=%s, port=%s, app=%s, args=%s",
req->tcUrl.c_str(), req->pageUrl.c_str(), req->swfUrl.c_str(),
req->schema.c_str(), req->vhost.c_str(), req->port.c_str(),
req->app.c_str(), (req->args? "(obj)":"null"));
// 获取connect命令带的参数
if(req->args) {
std::string srs_version;
std::string srs_server_ip;
int srs_pid = 0;
int srs_id = 0;
SrsAmf0Any* prop = NULL;
if ((prop = req->args->ensure_property_string("srs_version")) != NULL) {
srs_version = prop->to_str();
}
if ((prop = req->args->ensure_property_string("srs_server_ip")) != NULL) {
srs_server_ip = prop->to_str();
}
if ((prop = req->args->ensure_property_number("srs_pid")) != NULL) {
srs_pid = (int)prop->to_number();
}
if ((prop = req->args->ensure_property_number("srs_id")) != NULL) {
srs_id = (int)prop->to_number();
}
srs_info("edge-srs ip=%s, version=%s, pid=%d, id=%d",
srs_server_ip.c_str(), srs_version.c_str(), srs_pid, srs_id);
if (srs_pid > 0) {
srs_trace("edge-srs ip=%s, version=%s, pid=%d, id=%d",
srs_server_ip.c_str(), srs_version.c_str(), srs_pid, srs_id);
}
}
// 后续操作
ret = service_cycle();
// 如果配置文件中有回调配置,这里会通过post方式对给配置中的每个url发送播放停止消息
http_hooks_on_close();
return ret;
}
接下来SrsRtmpConn::service_cycle(),主要完成rtmp连接阶段的命令交互:1、发送确认窗口大小 2、发送设置带宽 3、接收客户端的确认窗口 4、根据配置设置chunk大小 5、发送命令消息中的“结果”(_result),通知客户端连接的状态
int SrsRtmpConn::service_cycle()
{
...
rtmp->set_window_ack_size((int)(2.5 * 1000 * 1000)))
...
rtmp->set_peer_bandwidth((int)(2.5 * 1000 * 1000), 2))
...
bandwidth->bandwidth_check(rtmp, skt, req, local_ip);
...
rtmp->set_chunk_size(chunk_size))
...
rtmp->response_connect_app(req, local_ip.c_str()))
...
rtmp->on_bw_done()
...
while (!disposed) {
ret = stream_service_cycle();
...
}
return ret;
}
接下来建立网络流和播放就都交给stream_service_cycle()来处理了,主要做的事情包括:
1)建立网络流
2)从tcUrl中解析出req的各个属性,比如host、app、stream等
3)获取SrsSource。在srs中,每个流都对应一个SrsSource,她是这条流相关的所有角色的纽带,比如(回源模式):回源服务play_edge、缓存gop_cache、消费者consumers(包换SrsConnection对应客户播放连接)等,都可以通过SrsSource来获取
4)服务器发送命令消息中的“响应状态”,告知客户端“播放”命令执行成功
5)调用playing(source)触发回源
int SrsRtmpConn::stream_service_cycle()
{
...
rtmp->identify_client(res->stream_id, type, req->stream, req->duration))
srs_discovery_tc_url(req->tcUrl, req->schema, req->host, req->vhost, req->app, req->stream, req->port, req->param);
...
SrsSource* source = NULL;
if ((ret = SrsSource::fetch_or_create(req, server, &source)) != ERROR_SUCCESS) {
return ret;
}
...
bool vhost_is_edge = _srs_config->get_vhost_is_edge(req->vhost);
bool enabled_cache = _srs_config->get_gop_cache(req->vhost);
...
source->set_cache(enabled_cache);
...
switch (type) {
case SrsRtmpConnPlay: {
...
rtmp->start_play(res->stream_id))
http_hooks_on_play()
...
ret = playing(source);
http_hooks_on_stop();
...
}
case SrsRtmpConnFMLEPublish: {
...
rtmp->start_fmle_publish(res->stream_id))
...
return publishing(source);
}
case SrsRtmpConnHaivisionPublish: {
...
rtmp->start_haivision_publish(res->stream_id))
...
return publishing(source);
}
case SrsRtmpConnFlashPublish: {
rtmp->start_flash_publish(res->stream_id))
...
return publishing(source);
}
default: {
ret = ERROR_SYSTEM_CLIENT_INVALID;
...
return ret;
}
}
return ret;
}
接着分析playing(source),看看对于rtmp播放请求,srs是怎么继续处理的。
int SrsRtmpConn::playing(SrsSource* source)
{
...
SrsConsumer* consumer = NULL;
source->create_consumer(this, consumer))
...
SrsQueueRecvThread trd(consumer, rtmp, SRS_PERF_MW_SLEEP);
// start isolate recv thread.
if ((ret = trd.start()) != ERROR_SUCCESS) {
return ret;
}
wakable = consumer;
ret = do_playing(source, consumer, &trd);
wakable = NULL;
trd.stop();
...
}
可以看到,这个函数主要做了三件事:
1)创建consumer,创建的同时,如果这条流对应的SrsSource已经在回源了,她会有meatadata、音频元信息、视频头元信息,可能还有有缓存的gop数据,所以在创建consumer的时候,会将SrsSource中的这些数据直接发送给客户端
2)创建一个接收微线程,我们知道客户端在播放rtmp的过程中是可以给服务端发送信令的,所以我们需要一个接受微线程去接受这些信令
3)最后一件事是playing(source)最重要的任务,就是触发回源。调用过程如下:
source->create_consumer(this, consumer)
|----play_edge->on_client_play()
|----ingester->start() // 当回源器是就绪状态的时候会调用
|----SrsEdgeIngester::cycle()
从上面的调用可以看到,创建consumer的时候,如果SrsSource对应的ingester的状态是就绪态,就会触发ingester启动独立的微线程去回源。独立的危险程究竟是如何回源的呢?让我们继续看SrsEdgeIngester::cycle(),这里实际上主要是作为一个rtmp客户端,去向上回源请求播放rtmp,这里的client实际上是一个rtmp客户端(注:如果我们想改造为支持多种协议回源,可以根据不同的req请求去初始化不同的类型的client),SrsEdgeIngester::cycle()只完成了作为rtmp客户端去向上回源时的连接和前期的信令交互,然后真正的回源交给了SrsEdgeIngester::ingest()去完成
int SrsEdgeIngester::cycle()
{
...
_source->on_source_id_changed(_srs_context->get_id());
...
connect_server(ep_server, ep_port))
...
client->set_recv_timeout(SRS_CONSTS_RTMP_RECV_TIMEOUT_US);
client->set_send_timeout(SRS_CONSTS_RTMP_SEND_TIMEOUT_US);
SrsRequest* req = _req;
client->handshake()
connect_app(ep_server, ep_port))
client->create_stream(stream_id))
client->play(req->stream, stream_id))
_edge->on_ingest_play()
ret = ingest();
...
}
下面来看看真正持续回源的循环体SrsEdgeIngester::ingest(),他是真正接收rtmp的msg的地方。前面说过回源是在一个独立的微线程里面来完成的,该微线程会循环向上读取msg,读取到msg就会通过process_publish_message(msg)交给SrsSource来分发到所有对应的consumer的发送队列中去。另外,client->recv_msg返回的是按rtmp协议从chunks中解析出来的msg,具体解析需要到SrsRtmpClient中去看
int SrsEdgeIngester::ingest()
{
...
client->set_recv_timeout(SRS_EDGE_INGESTER_TIMEOUT_US);
...
while (!pthread->interrupted()) {
...
SrsCommonMessage* msg = NULL;
client->recv_message(&msg))
...
SrsAutoFree(SrsCommonMessage, msg);
process_publish_message(msg))
}
...
}
刚才分析的playing(source),重点分析了里面会通过source->create_consumer(this, consumer)去触发一个独立的微线程去向上回源。但是播放连接的处理线程会继续调用do_playing(source, consumer, &trd)去将音视频数据下发给客户端,下面是do_playing的代码解析,可以看到,默认情况下,播放连接的处理微线程默认情况下会每隔mw_sleep毫秒从其对应的consumer的发送队列中读取新的msg(这些msg是回源微线程经过SrsSource写入到该队列中的),并发送给客户端。另外,在每次循环中也会查看有没有收到客户端发送的信令,主要会处理close和pause命令。
int SrsRtmpConn::do_playing(SrsSource* source, SrsConsumer* consumer, SrsQueueRecvThread* trd)
{
...
SrsMessageArray msgs(SRS_PERF_MW_MSGS);
...
while (!disposed) {
if (expired) {
return ret;
}
while (!trd->empty()) {
SrsCommonMessage* msg = trd->pump();
if ((ret = process_play_control_msg(consumer, msg)) != ERROR_SUCCESS) {
return ret;
}
}
if ((ret = trd->error_code()) != ERROR_SUCCESS) {
return ret;
}
mer->wait(SRS_PERF_MW_MIN_MSGS, mw_sleep);
}
...
consumer->dump_packets(&msgs, count))
...
if (count <= 0) {
st_usleep(mw_sleep * 1000);
continue;
}
...
rtmp->send_and_free_messages(msgs.msgs, count, res->stream_id))
...
if (send_min_interval > 0) {
st_usleep((int64_t)(send_min_interval * 1000));
}
}
return ret;
}
到这里,rtmp播放的整条链路就已经串联起来了!!!
目前原生srs的回源模式,只支持rtmp协议的回源方式。如果需要支持多种协议,可以通过提供不同的ingester子类型来实现。