P2P直播客户端RsPlayer的设计

0x1 客户端总体框架

传统的直播客户端只能从流媒体服务器下载流媒体数据,RsPlayer作为一个P2P(peer to peer)的直播客户端,可以同时从流媒体服务器和其他客户端请求媒体数据,比传统的直播客户端相比可以节省流媒体服务器的带宽.

下图显示了客户端的框架图.

最上层是UI层,提供用户的交互操作,用来显示播放视频窗口.

然后是P2PCore层,该层采用P2P方式接收和发送媒体数据.

P2PCore的下面是network层,用来处理socket接收和发送.

ffmpeg层用来处理媒体数据,音视频分离,音视频解码,音视频输出等.

architecture

0x2 P2PCore模块

如下图所示,p2pcore模块需要和三类服务器进行通讯,先从web server得到需要播放的流媒体链接,然后从tracker得到super peer的相关信息,然后再连接到super peer下载所需要的直播流数据,同时也会从其他p2pcore处下载直播流数据.

下面会详细介绍一下和这三类服务器的交互,也会介绍一下和其他p2pcore的交互过程.
p2pcore

与web服务器交互

RsStreamer把p2p直播流链接存储在web服务器上,RsPlayer读取存储在web服务器上的p2p直播流链接,然后解析该链接,从中读取tracker服务器的地址,然后建立和tracker服务器的tcp连接.

与tracker服务器交互

p2pcore发送登录消息到tracker服务器.

p2pcore把p2p直播流链接(从web服务器下载得到)解析得到的直播流信息发送给tracker服务器.

tracker服务器返回消息给p2pcore,告诉将要播放的直播流对应的super peer地址信息,同时也告诉播放同一直播流的其他p2pcore的地址信息.

p2pcore得到super peer地址以后,请求与其建立连接,并请求其发送相应的媒体数据.

p2pcore得到其他p2pcore地址以后也会请求与其建立相应的连接.

p2pcore还会定期向tracker服务器请求直播流的相关信息, 如当前播放的数据包的编号.

tracker服务器发送信息告诉p2pcore当前直播播放的数据包的编号.

与super peer交互

p2pcore建立和super peer的连接以后,把需要播放的直播流资源的信息发送给super peer, 并把直播流的数据包编号发送给super peer.

super peer然后把需要播放的直播流的头文件信息发送给p2p client, 然后根据其请求的资源编号发送直播流数据包.

上面提到了直播流的数据包编号,这个编号用来标记直播数据包,可以在p2p服务器和客户端之间统一管理直播数据. 如客户端需要哪个数据包,只需要把编号发送给服务器或其他客户端.

与其他p2pcore交互

这部分是体现RsPlayer直播客户端中p2p的部分,RsPlayer的p2pcore模块需要分析哪些数据需要从super peer得到,哪些数据需要从其他p2pcore处得到。

p2pcore建立和其他p2pcore的连接以后,需要查询其他p2pcore上有哪些数据包,其他p2pcore把其拥有的数据包信息发送过来以后,RsPlayer经过分析,决定需要从其接收哪些数据包,然后把需要的数据包信息发送给其他p2pcore, 其他p2pcore根据其请求的数据包编号发送相应的媒体数据包.

0x3 多媒体解码模块

p2pcore模块中的接收线程接收网络码流放到buffer中,ffmpeg中的解码线程从buffer中取出码流并解码输出.
buffer_manager

ffmpeg介绍

ffmpeg是一套开源的音视频处理库,包括文件格式处理库libavformat, 音视频编解码库libavcodec等,,目前各大视频网站提供的应用程序中都集成了ffmpeg.

p2pcore模块与ffmpeg模块的交互

修改ffmpeg中libavformat的代码,在ffmpeg中添加buffer protocol的输入接口,通过调用buffer protocol的输入接口,把外部读取buffer的函数指针作为参数传入到ffmpeg中,ffmpeg的解码线程需要数据的时候就调用这个函数指针,从而读取到需要的码流数据.

ffmpeg中添加buffer protocol的输入接口代码如下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/* buffer protocol */
static int buff_open(URLContext *h, const char *filename, int flags)
{
int fd;
char *context = strstr(filename,"context");
av_strstart(filename, "buff:", &filename);
fd = atoi(filename);
h->priv_data = (void *) (intptr_t) fd;
av_strstart(context, "context:", &context);
h->priv_data2 = (void *) (atoi(context));
h->is_streamed = 1;
return 0;
}
typedef int (*read_callback_fcn)(void* param, unsigned char *buf, int size);
static int buff_read(URLContext *h, unsigned char *buf, int size)
{
read_callback_fcn read_callback = (read_callback_fcn ) h->priv_data;
return read_callback(h->priv_data2, buf, size);
}
static int buff_write(URLContext *h, unsigned char *buf, int size)
{
return 0;
}
URLProtocol buff_protocol = {
"buff",
buff_open,
buff_read,
buff_write,
NULL,
NULL,
NULL,
NULL,
NULL,
file_get_handle
};

如何调用这个buffer protocol呢,我们的播放采用ffplay的流程,把按如下所示生成的mediafeeder_str作为ffplay的参数即可,mediafeeder_context是下面提到的类mediadatafeeder的对象指针,mediafeeder_handle是下面提到的类mediadatafeeder成员函数readdata()指针. ffplay先解析mediafeeder_str的内容,根据buff关键字匹配到buffer protocol的输入接口, buffer protocol的输入接口然后来调用mediadatafeeder::readdata().

1
sprintf(mediafeeder_str,"buff:%dcontext:%d\0",mediafeeder_handle, mediafeeder_context);

前面提到ffmpeg解码的时候会调用buffer protocol中的buff_read()函数,然后再调用p2pcore中的mediadatafeeder::readdata ()函数. 然后再调用livebuffermgr::getblock()读取到码流.

下面是在p2pcore中实现buffer读取的函数.

1
2
3
4
int mediadatafeeder::readdata(void* param, unsigned char* buffer, int size)
{
}

p2pcore中的buffer管理如下.
getblock()接口供上面的readdata函数调用, 相当于ffmpeg线程中调用该函数.
putblock()会被p2pcore中的网络接收线程调用.

由于livebuffermgr的getblock()和putblock()会被不同的线程调用,所以里面的buffer数据访问需要加锁以避免多线程之间的数据访问冲突.

1
2
3
4
5
6
void livebuffermgr::getblock(int blockID, int blockSize, unsigned char* data)
{
}
void livebuffermgr::putblock(int blockID, int blockSize, unsigned char* data)
{
}

0x4 在Android平台上的实现

在Android平台上实现RsPlayer客户端的话,p2pcore和ffmpeg都封装成相应的so库, 需要设置Android NDK开发环境, 采用Android Studio来开发.