协议圣经
协议圣经一是基础,还没有写,先出2
RTP组播
RTP为半应用层,半传输层协议,可以使用tcp,也可以使用udp,组播为D类地址,为何要使用组播,组播对什么有效,如224.3.4.5,端口选择一个9200,组播的好处是:
1 由交换机和路由器来确定发包
2 不用服务器转发
3 在udp基础上发送方便
缺点:不能跨网,不能到外网上去组播,也就是一旦确定组播方式,一般也就确定了是局域网程序,这里说一般,是因为是有mbone这种internet组播的,但是是实验爱好者组成的网,并不适合实际情况。
1、h264 h265 视频组播
h264编码和h265编码使用libx264 libx265 或者直接ffmpeg来编码,ffmpeg作为世界一等一的开源项目,封装了各类硬件和软件音视频编码和解码。
2、aac opus 音频组播
aac,opus 相对视频简单,不用分包,都小于MTU的大小
3、界面编写:
使用MFC c++来编写实例代码,这个相对简陋,有能力的可以使用任何界面,甚至跨个平台去编写,这里是示例,以方便调试展示为主。
3.1 采集
使用directshow去采集音频和视频,这个在windows上已经成熟彻底,也可以使用桌面来作为视频源,这个不做要求,主要是讲原理
3.2 设置
设置相对简单,后期示例需要加上窗口来写入码率,帧率设置,音频的一些设置
播放
其实播放是比较难做的,但是我们并不是为了制作播放器,我们是为了说明协议的发送,所以播放我们使用了一个非常简单的方法,vlc,写一个sdp文件,这里暂时只用视频来说明
m=video 9200 RTP/AVP 96
a=rtpmap:96 H264/90000
a=framerate:20
c=IN IP4 234.5.6.7
就这么简单,很奇特吧,端口在9200上面,映射payload为96 ,时间戳基数为 90000,如果你不想为90000,没问题的,改程序就可以,比如帧率是整数的20帧,你完全可以用一个绝对时间戳来作为RTP时间戳。组播IP地址为234.5.6.7,ok,启动程序后,使用vlc打开这个sdp,视频出现,播放器省了。一个好的benifit是,不用服务器就可以让跟多网内的人接收
m=video 9200 RTP/AVP 96 指明视频端口9200
a= framerate:20 指明帧率20
c=IN IP4 234.5.6.7 指明组播地址234.5.6.7
rtp协议
毋庸置疑,rtp,real time protocol 实时传输协议是传输协议的基础,使用RTP over UDP,如webrtc等都是使用这些东西来做的,rtp协议RFC文档:RFC 3550,不过还有很多其他文档,一般我们按照一个基础来做。
一般我们使用12个字节头部来说明rtp,比较重要的是时间戳和ssrc,注意,rtp头部并不一定是12字节,可以扩展。ssrc代表着一个唯一的流号码,如果多路流使用同样一组ssrc,那图像就要错了。但是同一路音频和视频是同一个ssrc的,否则谁也不知道哪个视频和哪个音频是要做合流。注意不能靠IP地址,因为一个IP地址可以发出多路流。
组播程序
我们只使用发送程序,使用asio来制作发送程序,并且使用同步发送,UDP其实非常奇怪,如果没有接收,bind 以后使用send其实是会有问题的,缓冲区满会停滞,所以我们使用sendto,即使是block也是不会停滞的,使用者放心不用担心发送阻塞的问题。以下给出组播发送的程序,组播接收的设置是不一样的,组播接收必须绑定组播的端口。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <asio.hpp>
#include <string>
using asio::ip::udp;
class c_multisock
{
public:
c_multisock():v_socket(v_service)
{}
void read()
{
asio::ip::udp::endpoint sender;
std::vector<char> buffer;
std::size_t bytes_readable = 0;
asio::socket_base::bytes_readable num_of_bytes_readable(true);
v_socket.io_control(num_of_bytes_readable);
// Get the value from the command.
bytes_readable = num_of_bytes_readable.get();
// If there is no data available, then sleep.
if (!bytes_readable)
{
return;
}
// Resize the buffer to store all available data.
buffer.resize(bytes_readable);
// Read available data.
v_socket.receive_from(
asio::buffer(buffer, bytes_readable),
sender);
// Extract data from the buffer.
std::string message(buffer.begin(), buffer.end());
// Output data.
std::cout << "Received message: ";
std::cout << message << std::endl;
}
void write(uint8_t *data, size_t len)
{
//char buffer[256];
//sprintf(buffer, msearchmsgfmt, "upnp:rootdevice");
/*socket.async_send_to(
boost::asio::buffer(buffer, strlen(buffer)), endpoint_,
boost::bind(handle_send_to, this,
boost::asio::placeholders::error));*/
v_socket.send_to(asio::buffer(data, len), v_destination);
}
int asio_init(const char * ip,uint16_t port, bool receiver)
{
asio::ip::address address =
asio::ip::address::from_string(ip); //"239.255.255.250"
//udp::socket socket(v_service);
v_socket.open(asio::ip::udp::v4());
// Allow other processes to reuse the address, permitting other processes on
// the same machine to use the multicast address.
v_socket.set_option(udp::socket::reuse_address(true));
// Guarantee the loopback is enabled so that multiple processes on the same
// machine can receive data that originates from the same socket.
v_socket.set_option(asio::ip::multicast::enable_loopback(true));
v_receiver = receiver;
v_socket.bind(
udp::endpoint(asio::ip::address_v4::any(),
receiver ? port /* same as multicast port */
: 16200/* any */));
v_destination = udp::endpoint(address, port);
// Join group.
v_socket.set_option(asio::ip::multicast::join_group(address));
// Start read or write loops based on command line options.
//if (receiver) read(socket);
//else write(socket, destination);
return 0;
}
private:
bool v_receiver;
udp::endpoint v_destination;
asio::io_context v_service;
udp::socket v_socket;
};
RTP协议
typedef void(*rtp_callback)(void *puser, uint8_t *data, int len);
#define PACKET_BUFFER_END (unsigned int)0x00000000
#define MAX_RTP_PKT_LENGTH 1400
#define H264 96
#define AAC 97
#define TEXT_ 98
#define PACKET_URL_ 177
typedef struct
{
/**//* byte 0 */
unsigned char csrc_len : 4;
unsigned char extension : 1;
unsigned char padding : 1;
unsigned char version : 2;
/**//* byte 1 */
unsigned char payload : 7;
unsigned char marker : 1;
/**//* bytes 2, 3 */
unsigned short seq_no;
/**//* bytes 4-7 */
unsigned long timestamp;
/**//* bytes 8-11 */
unsigned long ssrc;
} RTP_FIXED_HEADER;
typedef struct {
//byte 0
unsigned char TYPE : 5;
unsigned char NRI : 2;
unsigned char F : 1;
} NALU_HEADER; /**//* 1 BYTES */
typedef struct {
//byte 0
unsigned char TYPE : 5;
unsigned char NRI : 2;
unsigned char F : 1;
} FU_INDICATOR; /**//* 1 BYTES */
typedef struct {
//byte 0
unsigned char TYPE : 5;
unsigned char R : 1;
unsigned char E : 1;
unsigned char S : 1;
} FU_HEADER; /**//* 1 BYTES */
//please do not over 1400 bytes,because we must portable with webrtc
#define RTP_LENGTH 1400
#define RTP_PADDING 20
enum { MAX_LENGTH = RTP_LENGTH + RTP_PADDING};
class H264_RTP
{
//enum{ total_msize =2500 };
private:
void * v_puser = NULL;
unsigned int _timestamp_increse;
unsigned int _ts_current;
unsigned short _seq_num;
unsigned char sendbuf[MAX_LENGTH];
unsigned int v_ssrc = 1001;//the default value must hash("live/1001")
rtp_callback _callback = nullptr;
protected:
void SendPacket(unsigned char * buffer, int len);
void Close();
public:
H264_RTP();
~H264_RTP(void);
void SetSSRC(unsigned int ssrc)
{
v_ssrc = ssrc;
}
//use callback to send data
void RegisterCallback(void *puser, rtp_callback callback,const char *livessrc);
void send(const uint8_t* data, int dsize, uint32_t ts);
};
RTP h264 实现
#define HASH_PRIME_MIDDLE 10001531
static int hash_add(const char* key, int prime)
{
int hash, i;
int len = (int)strlen(key);
for (hash = len, i = 0; i < len; i++)
hash += key[i];
return (hash % prime);
}
H264_RTP::H264_RTP(void)
{
_seq_num = 0;
_ts_current = 0;
}
H264_RTP::~H264_RTP(void)
{
}
void H264_RTP::RegisterCallback(void *puser,rtp_callback callback, const char *livessrc)
{
v_puser = puser;
_callback = callback;
v_ssrc = hash_add(livessrc, HASH_PRIME_MIDDLE);
}
void H264_RTP::Close()
{
}
void H264_RTP::SendPacket(unsigned char * buffer, int len)
{
if (_callback != NULL)
{
_callback(v_puser, buffer, len);
}
}
//小端转大端
uint32_t little2big(uint32_t le) {
return (le & 0xff) << 24
| (le & 0xff00) << 8
| (le & 0xff0000) >> 8
| (le >> 24) & 0xff;
}
//大端转小端
uint32_t big2little(uint32_t be)
{
return ((be >> 24) & 0xff)
| ((be >> 8) & 0xFF00)
| ((be << 8) & 0xFF0000)
| ((be << 24));
}
//小端转大端
uint16_t little2bigs(uint16_t num)
{
uint16_t swapped = (num >> 8) | (num << 8);
return swapped;
}
//大端转小端
uint16_t big2littles(uint16_t be)
{
short swapped = (be << 8) | (be >> 8);
return swapped;
}
void H264_RTP::send(const uint8_t * data,int len,uint32_t ts)
{
// static
_ts_current = ts;
memset(sendbuf,0,12);
RTP_FIXED_HEADER *rtp_hdr; //RTP头部
NALU_HEADER *nalu_hdr; //NALU头部
FU_INDICATOR *fu_ind;
FU_HEADER *fu_hdr;
rtp_hdr =(RTP_FIXED_HEADER*)&sendbuf[0];
rtp_hdr->payload = H264;
rtp_hdr->version = 2;
rtp_hdr->marker = 0;
rtp_hdr->ssrc = little2big(v_ssrc);
// printf("rtp seqnum=%d\n",_seq_num);
char FirstByte = data[0];
int F = FirstByte & 0x80; //1 bit
int NRI = FirstByte & 0x60; // 2 bit
int TYPE = FirstByte & 0x1f;// 5 bit
//当一个NALU小于1400字节的时候,采用一个单RTP包发送
if(len<=1400)
{
//设置rtp M 位;
// DEBUG("n<1400");
rtp_hdr->marker=1;
// printf("rtp seqnum=%d\n",_seq_num);
rtp_hdr->seq_no = little2bigs(_seq_num ++);
nalu_hdr =(NALU_HEADER*)&sendbuf[12];
nalu_hdr->F= F;
nalu_hdr->NRI=NRI>>5;。
nalu_hdr->TYPE=TYPE;
unsigned char *nalu_payload=&sendbuf[13];
memcpy(nalu_payload,data+1,len-1);
rtp_hdr->timestamp= little2big(_ts_current);
int bytes=len + 12 ;
SendPacket(sendbuf, bytes);
}
else if(len>1400)
{
//得到该nalu需要用多少长度为1400字节的RTP包来发送
int k=0,l=0;
pagenum = (len+1400-1)/1400
k=len/1400;
l=len%1400;
int t=0;
rtp_hdr->timestamp= little2big(_ts_current);
while(t<=k)
{
if(!t)
{
rtp_hdr->marker=0;
fu_ind =(FU_INDICATOR*)&sendbuf[12];
fu_ind->F=F;
fu_ind->NRI=NRI>>5;
fu_ind->TYPE=28;
//设置FU HEADER,并将这个HEADER填入sendbuf[13]
fu_hdr =(FU_HEADER*)&sendbuf[13];
fu_hdr->E=0;
fu_hdr->R=0;
fu_hdr->S=1;
fu_hdr->TYPE=TYPE;
rtp_hdr->seq_no = little2bigs(_seq_num ++);
// printf("head nalu\n");
unsigned char *nalu_payload=&sendbuf[14];//同理将sendbuf[14]赋给nalu_payload
memcpy(nalu_payload, data +1,1400);//去掉NALU头
int bytes=1400+14;
SendPacket(sendbuf, bytes);
t++;
}
else if(k==t)
{
rtp_hdr->marker=1;
fu_ind =(FU_INDICATOR*)&sendbuf[12];
fu_ind->F=F;
fu_ind->NRI=NRI>>5;
fu_ind->TYPE=28;
//设置FU HEADER,并将这个HEADER填入sendbuf[13]
fu_hdr =(FU_HEADER*)&sendbuf[13];
fu_hdr->R=0;
fu_hdr->S=0;
fu_hdr->TYPE=TYPE;
fu_hdr->E=1;
if(l-1>=0)
{
rtp_hdr->seq_no = little2bigs(_seq_num ++);
unsigned char *nalu_payload=&sendbuf[14];
memcpy(nalu_payload, data +t*1400+1,l-1);
int bytes = l+14-1;
SendPacket(sendbuf, bytes);
}
t++;
}
else if(t<k&&0!=t)
{
rtp_hdr->marker=0;
fu_ind =(FU_INDICATOR*)&sendbuf[12];
fu_ind->F=F;
fu_ind->NRI=NRI>>5;
fu_ind->TYPE=28;
//设置FU HEADER,并将这个HEADER填入sendbuf[13]
fu_hdr =(FU_HEADER*)&sendbuf[13];
//fu_hdr->E=0;
fu_hdr->R=0;
fu_hdr->S=0;
fu_hdr->E=0;
fu_hdr->TYPE=TYPE;
rtp_hdr->seq_no = little2bigs(_seq_num ++);
unsigned char *nalu_payload=&sendbuf[14];
// printf("length - %u\n",t*1400+1);
memcpy(nalu_payload, data +t*1400+1,1400);
int bytes=1400+14;
SendPacket(sendbuf, bytes);
t++;
}
}
}
}
以上使用h264,rtp,asio写的组播程序来描述协议,实际上,要写一个完整的代码,还是需要一定的时间的,如果读者有什么需求,可以随时和作者交流,未完待续。