基于VLC的音视频项目笔记

项目简介:

基于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交互过程详解:

  1. 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
  1. 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
...各种属性描述
  1. 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端口。

  1. 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)发送数据

  1. 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未调用

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值