项目简介:
基于VLC库实现的音视频播放器(客户端+服务器),有播放、暂停/继续、停止、进度显示、跳转位置、地址输入、音量调节等基本功能。
学习RTSP协议的流程,处理H.264分片。
VLC概述:
部分代码整理:
1,VLC库的使用(关键库函数)
要注意的是实例、媒体、播放器的声明与释放,先声明的要后释放。
2,客户端设计
因为客户端采用MFC做界面,所以考虑使用MVC设计模式进行开发,将数据与界面分类,由中间的Controller层负责两边的交互,客户端比较简单,就不贴代码了,大致类图如下:
3,客户端开发的问题
在处理播放进度条和音量条的拖动消息的时候,发现无法响应如下消息:
仔细核对事件处理程序的介绍,并没有发现什么问题
后面又查了一些资料,了解到另一组响应滚动条的消息
WM_HSCROLL;//水平滚动条
WM_VSCROLL;//垂直滚动条
void CVideoClientDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)//水平滚动条信息机制(对应播放位置信息)
void CVideoClientDlg::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)//垂直滚动条信息机制(对应音量信息)
添加上述消息的响应后,测试正常。
4,RTSP协议
RTSP协议是基于RTP和RTCP之上的,RTSP使用RTP传输媒体数据,使用RTCP来交互控制命令,整个协议是先用RTSP进行信息交互,然后再使用RTP/RTCP进行媒体数据传输。
RTSP的控制命令交互格式:
//客户端的请求格式:
Method url version\r\n
CSeq: x\r\n
xxx\r\n
...
\r\n
//服务器的响应格式:
Version 200 OK\r\n
CSeq: x\r\n
Xxx\r\n
...
\r\n
其中:method表示方法(项目中使用了五种基本的方法:OPTIONS、DESCRIBE、SETUP、PLAY、TEARDOWN);
Url就是请求地址,一般为rtsp://ip:port/session 默认554端口 常见:8554端口;
Version 表示版本,我们这里取RTSP/1.0;
CSeq序列号,递增的整数;
200遵循http协议的状态码 200表明成功。
RTSP交互过程详解:
- OPTIONS方法:获取服务端提供的可用方法
//客户端C向服务器S发送OPTIONS
OPTIONS rtsp://127.0.0.1:554/live RTSP/1.0\r\n
CSeq: 1\r\n
\r\n
//服务器S收到后,回应客户端C,告诉客户端可用的方法
RTSP/1.0 200 OK\r\n
CSeq: 1\r\n
Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY\r\n
\r\n
- DESCRIBE方法:获取对应会话的媒体描述信息
//然后客户端会向服务器发送DESCRIBE命令,获取媒体的描述信息
DESCRIBE rtsp://127.0.0.1:554/live RTSP/1.0\r\n
CSeq: 2\r\n
Accept: application/sdp\r\n
\r\n
//服务器应答
RTSP/1.0 200 OK\r\n
CSeq: 2\r\n
Content-length: xxx\r\n
Content-type: application/sdp\r\n
\r\n
v=0\r\n
...各种属性描述
- SETUP方法:向服务器发起建立请求,建立连接会话
//然后客户端就会发送SETUP命令
SETUP rtsp://127.0.0.1:554/live/track0 RTSP/1.0r\r\n
CSeq: 3\r\n
Transport: RTP/AVP;unicast;client_port=50000-50001\r\n
\r\n
//服务器应答
RTSP/1.0 200 OK\r\n
CSeq: 3\r\n
Transport: RTP/AVP;unicast;client_port=50000-50001;server_port=55000-55001\r\n
Session: 12345678\r\n
\r\n
其中:RTP/AVP表示RTP通过UDP发送,如果是RTP/AVP/TCP则表示RTP通过TCP发送;
Unicast表示单播,multicast表示多播;
Client_port表示50000是RTP端口50001表示RTCP端口,都是UDP套接字;
Server_port表示服务器使用这两个端口传输数据,其中55000是RTP,55001是RTCP端口。
- PLAY方法:向服务器发起播放请求
//客户端收到这个之后,就可以开始发送PLAY命令
PLAY rtsp://127.0.0.1:554/live RTSP/1.0\r\n
CSeq: 4\r\n
Session: 12345678\r\n
Range: npt=0.000-\r\n
\r\n
//服务器应答
RTSP/1.0 200 OK\r\n
CSeq: 4\r\n
Range: npt=0.000-\r\n
Session: 12345678; timeout=60\r\n
\r\n
服务器回复完这个数据之后,会向客户端的RTP端口(50000)发送数据
- TEARDOWN方法:向服务器发起关闭连接会话请求
//最后,客户端要关闭连接的时候,就发送TEARDOWN命令
TEARDOWN rtsp://127.0.0.1:554/live RTSP/1.0\r\n
CSeq: 5\r\n
Session: 12345678\r\n
\r\n
//服务器应答
RTSP/1.0 200 OK\r\n
CSeq: 5\r\n
\r\n
基于上述交互协议的介绍,所以服务器这边写了一个处理客户端请求的函数:
RTSPRequest RTSPSession::AnalyseRequest(const EBuffer& buffer)
{
RTSPRequest request; // 解析出来的请求就会放在这里并作为结果返回
if (buffer.size() <= 0)
return request;
EBuffer data = buffer;
// 取出data(即buffer)内容的第一行,依据每一行结尾处都是\r\n分割数据
EBuffer line = PickOneLine(data);
EBuffer method(32), url(1024), version(16), seq(64); // 分别声明几个变量并给予一定数量的比特位
if (sscanf(line, "%s %s %s\r\n", (char*)method, (char*)url, (char*)version) < 3)
{
TRACE("Error at : [%s]\r\n", (char*)line);
return request;
}
// 否则代表读取成功,那就该处理数据了
line = PickOneLine(data); // 第二次调用PickOneLine函数就将取data(即buffer)第二行的内容
if (sscanf(line, "CSeq: %s\r\n", (char*)seq) < 1)
{
TRACE("Error at : [%s]\r\n", (char*)line);
return request;
}
// 对取出来的内容进行一下处理
request.SetMethod(method);
request.SetUrl(url);
request.SetSequence(seq);
// 然后比较方法是哪个方法
if ((strcmp(method, "OPTIONS") == 0) || (strcmp(method, "DESCRIBE") == 0))
return request; // 这俩方法 不需要处理什么,所以直接返回就行
if (strcmp(method, "SETUP") == 0) // setup方法需要稍微处理一下
{
do
{
line = PickOneLine(data); // 又继续往下读了一行
if (strstr((const char*)line, "client_port=") == NULL)
continue;
break;
} while (line.size() > 0);
int port[2] = { 0,0 }; // 端口数组
if (sscanf(line, "Transport: RTP/AVP;unicast;client_port=%d-%d\r\n", port, port + 1) == 2) // 读取成功 就把两个端口数据放到端口数组里
{
request.SetClientPort(port); // 处理下端口值
return request; //就返回就行了
}
}
if ((strcmp(method, "PLAY") == 0) || (strcmp(method, "TEARDOWN") == 0)) // 对这俩方法的处理情况一样
{
line = PickOneLine(data); // 继续下读一行
EBuffer session(64); // 用于承载读出来的session值
if (sscanf(line, "Session: %s\r\n", (char*)session) == 1) // 读取成功就把session放进去
{
request.SetSession(session); // 处理一下
return request; // 就可以返回了
}
}
return request;
}
//上述函数返回的request会在这里处理并回以客户端应答
{
RTSPRequest req = AnalyseRequest(buffer);
if (req.method() < 0) // 如果拿到的结果的方法出错
{
TRACE("buffer [%s]\r\n", buffer);
return -2;
}
RTSPReply rep = Reply(req); // 成功拿到req后就把它丢进Reply()里,rep为对请求的应答
ret = m_client.Send(rep.toBuffer()); // 有了应答就把应答发送出去
if (req.method() == 2)
m_port = (short)atoi(req.port());
if (req.method() == 3)
cb(thiz, *this);
}
5,RTP协议
二进制协议,最重要的就是内容成分组成(嵌套式)和位宽。
RTP包由一个头部和数据荷载/数据负载组成。
头部格式及说明如下:
所以,根据RTP头部格式,其实现为:
数据荷载部分:
6,H.264解析
H.264是一种流媒体(是一种近乎无限可分的文件格式),其格式如下:
每个NAL的起始码为00 00 00 01或者00 00 01,所以可以此为依据对H.264进行处理。
寻找H.264头的代码如下:
long MideaFile::FindH264Head(int& headSize)
{
while (!feof(m_file))
{
char c = 0x7F;
//没有读到文件结尾 eof:end of file
while (!feof(m_file))
{
//H264每一段以00 00 00 01 或00 00 01开头,所以需要找0的位置
c = fgetc(m_file);
if (c == 0)
break;
}
//在文件结尾前读到一个0跳出while后(第一个0)
if (!feof(m_file))
{
//继续读下一个
c = fgetc(m_file);
if (c == 0)//(第二个0)
{
c = fgetc(m_file);
if (c == 1)//读到了一个文件头
{
headSize = 3;
return ftell(m_file) - 3;// 00 00 01情况:找到1后 说明头部的位置应该是1的位置向前3个字节 (下面-4同理)
}
if (c == 0)//(第三个0)
{
c = fgetc(m_file);
if (c == 1)//读到了一个文件头
{
headSize = 4;
return ftell(m_file) - 4;
}
}
}
}
}
return -1;
}
这样,找到头部以后就可以准确的对其进行分割处理。
发送:
解析完成之后,需要将其发送给客户端,发送时涉及到组包的问题,大致代码如下:
int RTPHelper::SendMediaFrame(RTPFrame& rtpFrame, EBuffer& frame, const EAddress& client)
{
//帧开头占的字节数
int sepSize = GetFrameSepSize(frame);
//帧数据长度
size_t frame_size = frame.size() - sepSize;
//指向数据开始处
BYTE* pFrame = sepSize + (BYTE*)frame;
//条件成立 说明需要分片
if (frame_size > MAX_RTPFRAME)
{
//取第一个字节的低5位
BYTE nalu = pFrame[0] & 0x1F;
//能分几个包
size_t count = frame_size / MAX_RTPFRAME;
//不能按照1300字节完整分包最后剩下的大小
size_t restsize = frame_size % MAX_RTPFRAME;
for (int i = 0; i < count; i++)
{
rtpFrame.m_pyload.resize(MAX_RTPFRAME + 2);
//操作第一个字节 0110 0000|0001 1100 = 0111 1100
((BYTE*)rtpFrame.m_pyload)[0] = 0x60 | 28;
//操作第二个字节 分情况 第一个包、中间的包、最后一个包
((BYTE*)rtpFrame.m_pyload)[1] = nalu; //中间 S置0 E置0 0 0 0 xxxxx
if(i == 0)
((BYTE*)rtpFrame.m_pyload)[1] |= 0x80;//开始 S置1 E置0 1 0 0 xxxxx
if((restsize == 0) && (i == count-1))
((BYTE*)rtpFrame.m_pyload)[1] |= 0x40;//结束 S置0 E置1 0 1 0 xxxxx
//从pFrame + MAX_RTPFRAME * i地址处拷贝MAX_RTPFRAME字节数据到rtpFrame.m_pyload中 前面的+2是特殊处理的2字节
memcpy(2 + (BYTE*)rtpFrame.m_pyload, pFrame + MAX_RTPFRAME * i + 1, MAX_RTPFRAME);
//发送
SendFrame(rtpFrame, client);
//序号变化 序号是累加的
rtpFrame.m_head.serial++;
}
//处理最后剩下的尾巴
if (restsize > 0)
{
rtpFrame.m_pyload.resize(restsize + 2);
((BYTE*)rtpFrame.m_pyload)[0] = 0x60 | 28;
((BYTE*)rtpFrame.m_pyload)[1] = nalu;
((BYTE*)rtpFrame.m_pyload)[1] |= 0x40;
memcpy(2 + (BYTE*)rtpFrame.m_pyload, pFrame + MAX_RTPFRAME * count + 1, restsize);
SendFrame(rtpFrame, client);
rtpFrame.m_head.serial++;
}
}
//正常包 可直接发送
else
{
rtpFrame.m_pyload.resize(frame.size() - sepSize);
memcpy(rtpFrame.m_pyload, pFrame, frame.size() - sepSize);
SendFrame(rtpFrame, client);
rtpFrame.m_head.serial++;
}
//时间戳变化 时间戳是计算出来的,从0开始 每帧追加(时钟频率90000/每秒帧数24)
rtpFrame.m_head.timestamp += 90000 / 24;
//发送完成后加入休眠,否则会出问题,因为UDP协议是不管投递质量的,可能一瞬间把所有包都发出去,要控制发送速度
Sleep(1000 / 24);
return 0;
}
代码的意义大部分已体现在注释中,不做过多赘述。
本项目中还有些内容是移植远程控制项目中的部分代码,这里也不做重复记录了。
7,服务器开发的问题
这种问题的处理,是因为头文件包含的顺序引起的,把套接字的头文件,放到windows.h文件的前面,或者尽可能放到引用头文件的前面即可解决。
解决方法:缺少库,#pragma comment(lib,“rpcrt4.lib”)
解决方法:缺少库,#pragma comment(lib, “ws2_32.lib”))
socket返回-1 而参数没有问题,那么有可能是WSAStartup未调用