VS2017工程源码:shawnsky/Live555MediaClient,也许可以帮到正好需要它的人
思路
思路很清晰,客户端发送 RTSP 请求到服务器,服务器就会用 RTP 发送媒体数据给客户端,客户端收到数据后保存到文件,再调用 VLC 库就可以播放音乐了。
所以,编写客户端就是要干这些事:
- 学习一下 RTSP 协议(RFC 2326)
- 通过 TCP Socket 依次发送 OPTIONS、DESCRIBE、SETUP、PLAY 请求(照着协议规定来拼接字符串)
- 起线程用 UDP Socket 接收 server 发来的 RTP 数据,直接写到本地文件
- 阅读 libvlc 文档,创建播放器实例播放指定文件
关于 RTSP 协议
RTSP 协议规范的请求格式参见 RFC 2326 Method definitions
封装 Winsock API
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
class MySocket
{
private:
CInitSock sock;
SOCKET socket;
public:
MySocket()
{
socket = INVALID_SOCKET;
}
bool Create(std::string protocol)
{
if (protocol == "TCP")
{
socket = ::socket(AF_INET, SOCK_STREAM, 0);
}
else
{
socket = ::socket(AF_INET, SOCK_DGRAM, 0);
}
if (socket == INVALID_SOCKET) {
return false;
}
return true;
}
bool Bind(int port)
{
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(port);
sin.sin_addr.s_addr = htonl(ADDR_ANY);
if (::bind(socket, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
{
return false;
}
return true;
}
bool Connect(char *ip, int port)
{
sockaddr_in server_sin;
int adddr_len = sizeof(server_sin);
/* set media server address */
memset((void*)&server_sin, 0, adddr_len);
server_sin.sin_family = AF_INET;
server_sin.sin_port = htons(port);
server_sin.sin_addr.S_un.S_addr = inet_addr(ip);
/* do connect */
if (::connect(socket, (sockaddr *)&server_sin, adddr_len) != 0)
{
return false;
}
return true;
}
int Send(std::string str)
{
int iResult;
iResult = ::send(socket, str.c_str(), str.size(), 0);
return iResult;
}
int TCPRecv(char *buff, int len)
{
int iResult;
iResult = ::recv(socket, buff, len, 0);
return iResult;
}
int UDPRecv(char *buff, int len)
{
int iResult;
/* no need to save sockaddr */
iResult = ::recvfrom(socket, buff, len, 0, NULL, NULL);
return iResult;
}
void Close()
{
::closesocket(socket);
socket = INVALID_SOCKET;
}
};
|
请求顺序
OPTIONS -> DESCRIBE -> SETUP -> PLAY -> TEARDOWN
发送 PLAY 之后间隔性的发送 GET_PARAMTER 请求保活(心跳机制)。
请求
客户端构造出对应字符串发送,然后用字符串操作来解析服务器返回的内容,如下是 RTSP SETUP 请求的格式,和 HTTP 差不多。SETUP 响应里会提供会话ID,之后的请求需要带着它。
# C->S
SETUP rtsp://example.com/foo/bar/baz.rm RTSP/1.0
CSeq: 302
Transport: RTP/AVP;unicast;client_port=4588-4589
# S->C
RTSP/1.0 200 OK
CSeq: 302
Date: 23 Jan 1997 15:35:06 GMT
Session: 47112344
Transport: RTP/AVP;unicast;client_port=4588-4589;server_port=6256-6257
我写了一个 RTSPReqHelper 辅助类,专门来处理所有 RTSP 请求和响应,如下是其中的 options 函数:
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
|
string RTSPReqHelper::options()
{
// Build Request
string line = "OPTIONS ";
line.append(url);
line.append(" RTSP/1.0\r\n");
line.append("CSeq: ");
line.append(to_string(seq++));
line.append("\r\n");
line.append("User-Agent: Cxt Media Client v0.1\r\n\r\n");
// Send over TCP
socket.Send(line);
// Handle Response
memset(recvBuff, 0, sizeof(recvBuff));
socket.TCPRecv(recvBuff, sizeof(recvBuff));
string s(recvBuff);
string cseq = parseValueOf(s, "CSeq: ");
string status = parseValueOf(s, " ");
string options = parseValueOf(s, "Public: ");
if (to_string(seq-1).compare(cseq) != 0 || status.compare("200 OK") != 0)
{
return "ERROR";
}
return options;
}
|
所以,需要编写一个发送 RTSP 请求的线程工作函数,顺序调用RTSPReqHelper 类中的请求方法,在 RTSP PLAY 请求得到 200 OK 的响应之后,再启动接收 RTP 数据的线程。这个 RTSP 请求线程会在用户点击「打开URL」按钮时触发。
关于 RTP 协议
了解 RTP 数据的格式是关键,可以参见 RFC 3550,找到如下请求头
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X| CC |M| PT | sequence number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| synchronization source (SSRC) identifier |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| contributing source (CSRC) identifiers |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
根据实际抓包测试,可以发现:
- Version, Padding, Extension, CSRC, Marker 占 1 byte
- Payload type 占 1 byte
- Sequence number 占 2 bytes
- Timestamp 占 4 bytes
- SSRC 占 4 bytes
根据 RFC 3551,可以知道 Payload 类型定义表(播放音频,对应的 PT = 4)
剩下的部分是 Payload,我需要把它们写入文件。
对于payload部分,还需要去查阅 RFC 2250,可以找到 MPEG Audio-specific header
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| MBZ | Frag_offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
可以看出,payload 的开始处(RTP header 的末尾)还有 4 个字节的 payload header。
因此,在保存媒体文件时,需要设置长度 16 字节的偏移量。
RTP 线程在启动后要创建一个 UDP Socket,并绑定一个本地端口号,这个端口号必须固定,因为 RTSP 的 SETUP 请求中提供了这个端口号(不然的话,server 不知道朝哪发 RTP)。
然后就是调用 recvfrom 函数接收 UDP 数据,并把 payload 写入到本地文件。
调用 VLC 实现音乐播放
照着 libvlc 的文档,就大概可以封装好一些简单的播放器 API:
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
class MyVlcPlayer
{
private:
libvlc_instance_t* vlc_ins = NULL;
libvlc_media_player_t* vlc_player = NULL;
libvlc_media_t* vlc_media = NULL;
libvlc_time_t total_time;
const char * vlc_args[3] = {
"-I", "dummy", /* Don't use any interface */
"--ignore-config" /* Don't use VLC's config */
};
public:
MyVlcPlayer(){}
~MyVlcPlayer()
{
libvlc_media_release(vlc_media);
libvlc_media_player_release(vlc_player);
libvlc_release(vlc_ins);
}
bool Initialize(string path)
{
// Create VLC instance
vlc_ins = libvlc_new(0, NULL);
if (vlc_ins == NULL)
{
return false;
}
vlc_player = libvlc_media_player_new(vlc_ins);
if (vlc_player == NULL)
{
return false;
}
// Set media file path
const char *p = path.c_str();
vlc_media = libvlc_media_new_path(vlc_ins, p);
if (vlc_media == NULL)
{
return false;
}
// Parse media
libvlc_media_parse(vlc_media);
// Get total time in ms
total_time = libvlc_media_get_duration(vlc_media);
// Set media to player
libvlc_media_player_set_media(vlc_player, vlc_media);
return true;
}
void Play()
{
libvlc_media_player_play(vlc_player);
}
void Stop()
{
libvlc_media_player_stop(vlc_player);
}
libvlc_time_t GetTotalTime()
{
return total_time;
}
};
|
注意需要正确配置项目,下载 vlc 的 sdk,设置 include 包含文件夹和 lib 库文件夹,然后把 libvlc.dll 和 libvlccore.dll 和 plugins 文件夹拷贝到项目 Debug 目录下,才可以正常播放。
线程同步
使用全局变量的方式,注意需要加上 volatile 关键字。
1
2
3
4
|
/* Thread state */
volatile int rtspThreadState = 0;
volatile int rtpThreadState = 0;
volatile int playerThreadState = 0;
|
线程同步的目的是保证 RTSP PLAY 收到 200 OK 响应之后才开始接收 RTP,然后收到第一个 RTP 数据并且已经完成文件创建以及第一次写入之后,才开始播放媒体文件。
相较于 WaitForSingleObject 的方式,我这次使用循环来等待线程状态,比如播放器线程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
UINT MyPlayFunction(LPVOID p)
{
for (;;) if (playerThreadState) break;
CString* ps = new CString("[INFO] Start playing...\r\n");
PostMessage(AfxGetMainWnd()->GetSafeHwnd(), WM_UPDATE_MSG, NULL, (LPARAM)ps);
CMediaClientDlg* dlg = (CMediaClientDlg*)p;
MyVlcPlayer player;
player.Initialize(dlg->m_filename);
player.Play();
for (;;)
{
// Terminate thread condition
if (!playerThreadState)
{
AfxEndThread(0);
}
}
return 0;
}
|