RTMP 直播是 SRS 最典型的使用场景,客户端使用 RTMP 协议向 SRS 推流,使用 RTMP 协议从 SRS 拉流,SRS 作为一个 RTMP 直播服务器实现媒体的转发。同时,RTMP 是 SRS 的中转协议,其他协议之间的互通需要先转为 RTMP,因此,理解 SRS RTMP 直播实现是理解其他协议实现的重要前提。本文主要分析 SRS RTMP 直播功能的实现原理,相关概念和配置请参考官方文档。
1. Origin
单机版 SRS,加上一个推流端和一个拉流端,组成最简单的 RTMP 直播场景,如下图所示。其中 RtmpConn 表示一个 RTMP 连接,LiveSouce 表示一个直播资源,接下来我们来分析这个直播场景的实现。
1.1. 创建连接
在初始化阶段,SrsServer 会读取配置文件使用 SrsMultipleTcpListeners 建立监听。SrsMultipleTcpListeners 把自己装扮成一个 ISrsListener,但其内部包含的 SrsTcpListener 才是真正的监听者,支持多个监听者。
SrsTcpLisener 启动后,当有客户端连接上来,SrsTcpLisener 会通知 SrsMultipleTcpListeners,SrsMultipleTcpListeners 再通知 SrsServer,SrsServer 创建一个 SrsRtmpConn,添加到 SrsResourceManger(用来管理所有 RTMP 连接),并启动 SrsRtmpConn 事件循环。Publisher 和 palyer 连接都是相同的处理流程。
相关源码如下:
if (!resource) {
if (listener == rtmp_listener_) {
resource = new SrsRtmpConn(this, stfd2, ip, port);
}
...
}
// 添加到容器进行集中管理
conn_manager->add(resource);
// 启动连接的事件循环,开始处理报文
ISrsStartable* conn = dynamic_cast<ISrsStartable*>(resource);
if ((err = conn->start()) != srs_success) {
return srs_error_wrap(err, "start conn coroutine");
}
1.2. 协议交互
连接建立成功后,下一步就要按照 RTMP 协议规范,完成 handshake、connect 等协议交互,当然还会有其他参数设置。
在 SrsRtmpConn 的事件循环中,可以看到 handshake 和 connect 动作。因为 RTMP 协议是 CS 模式,不管是推流还是拉流,都是客户端主动发起连接,然后执行收发媒体之前的协议交互,处理逻辑是一样的。
srs_error_t SrsRtmpConn::do_cycle()
{
...
// 完成 handshake
if ((err = rtmp->handshake()) != srs_success) {
return srs_error_wrap(err, "rtmp handshake");
}
...
// 完成 connect
SrsRequest* req = info->req;
if ((err = rtmp->connect_app(req)) != srs_success) {
return srs_error_wrap(err, "rtmp connect tcUrl");
}
...
// 进入接收媒体数据循环
if ((err = service_cycle()) != srs_success) {
err = srs_error_wrap(err, "service cycle");
}
...
}
1.3. 识别类型
在处理媒体数据之前,需要能识别客户端是 publisher 还是 player。SRS 根据具体协议定义了一系列连接类型,分为 publishers 和 players 两大类。我们当前分析的场景,只需要关注 SrsRtmpConnPlay 和 SrsRtmpConnFMLEPublish 两种类型即可。
enum SrsRtmpConnType
{
SrsRtmpConnUnknown = 0x0000,
// All players.
SrsRtmpConnPlay = 0x0100,
SrsHlsPlay = 0x0101,
SrsFlvPlay = 0x0102,
SrsRtcConnPlay = 0x0110,
SrsSrtConnPlay = 0x0120,
// All publishers.
SrsRtmpConnFMLEPublish = 0x0200,
SrsRtmpConnFlashPublish = 0x0201,
SrsRtmpConnHaivisionPublish = 0x0202,
SrsRtcConnPublish = 0x0210,
SrsSrtConnPublish = 0x0220,
};
SRS 先调用 identify_client 方法识别客户端类型,然后根据客户端类型确定是做推流处理还是拉流处理。
srs_error_t SrsRtmpConn::stream_service_cycle()
{
// 解析客户端类型
if ((err = rtmp->identify_client(info->res->stream_id, info->type, req->stream,
req->duration)) != srs_success) {
return srs_error_wrap(err, "rtmp: identify client");
}
...
// 根据客户端类型进行对应处理
switch (info->type) {
case SrsRtmpConnPlay: {
...
return playing(source); // 拉流
}
case SrsRtmpConnFMLEPublish: {
...
return publishing(source); // 推流
}
...
}
...
}
identify_client 根据收到的消息来判定客户端类型,推流端才会发送以下消息:
#define RTMP_AMF0_COMMAND_RELEASE_STREAM "releaseStream"
#define RTMP_AMF0_COMMAND_FC_PUBLISH "FCPublish"
#define RTMP_AMF0_COMMAND_UNPUBLISH "FCUnpublish"
拉流端才会发送以下消息:
#define RTMP_AMF0_COMMAND_PLAY "play"
srs_error_t SrsRtmpServer::identify_client(int stream_id, SrsRtmpConnType& type,
string& stream_name, srs_utime_t& duration)
{
while (true) {
SrsCommonMessage* msg = NULL;
if ((err = protocol->recv_message(&msg)) != srs_success) {
return srs_error_wrap(err, "recv identify message");
}
...
if (dynamic_cast<SrsCreateStreamPacket*>(pkt)) {
// 内部做进一步分析
return identify_create_stream_client(
dynamic_cast<SrsCreateStreamPacket*>(pkt), stream_id, 3, type,
stream_name, duration);
}
if (dynamic_cast<SrsFMLEStartPacket*>(pkt)) {
// type = SrsRtmpConnFMLEPublish
return identify_fmle_publish_client(
dynamic_cast<SrsFMLEStartPacket*>(pkt), type, stream_name);
}
if (dynamic_cast<SrsPlayPacket*>(pkt)) {
// type = SrsRtmpConnPlay
return identify_play_client(dynamic_cast<SrsPlayPacket*>(pkt), type,
stream_name, duration);
}
}
}
1.4. Publish
如果识别是连接的客户端是 publisher,进入 publishing 处理流程。
1.4.1. 准备工作
推流端发送 publish 消息,SrsRtmpConn 会创建一个 SrsLiveSource,SrsLiveSource 是直播源的逻辑实体,每个推流在 SRS 上都会有一个 SrsLiveSource 对应。
srs_error_t SrsRtmpConn::stream_service_cycle()
{
...
// 可能需要创建 SrsLiveSource
SrsLiveSource* source = NULL;
if ((err = _srs_sources->fetch_or_create(req, server, &source)) != srs_success) {
return srs_error_wrap(err, "rtmp: fetch source");
}
switch (info->type) {
...
case SrsRtmpConnFMLEPublish: {
if ((err = rtmp->start_fmle_publish(info->res->stream_id)) != srs_success) {
return srs_error_wrap(err, "rtmp: start FMLE publish");
}
return publishing(source);
}
...
}
return err;
}
可能是基于性能考量,SRS 创建了一个新的协程来专门接收推流媒体数据,一切准备妥当,通知客户端可以发送媒体数据了,进入 SrsRtmpConn 的事件循环。
srs_error_t SrsRtmp