概述
实现一个简单的RTSP服务器,主要用于从本地AAC文件读取音频数据,然后通过RTP协议实时传输AAC音频流。整体结构和H264视频流服务器结构相似
ADTS头部
结构体分析
该结构体主要用于描述ADTS头部,该头部信息位于每个AAC音频帧之前,其中包含了音频帧的同步信息、长度、采样率等重要参数
syncword
: 同步字 (0xFFF),标志 ADTS 帧的开始。aacFrameLength
: ADTS 帧长度 (包括头部和 AAC 原始数据),这是确定每个 AAC 帧大小的关键信息。samplingFreqIndex
: 采样率索引,对应实际的采样率 (如 44100Hz, 48000Hz)。channelCfg
: 声道配置,表示音频声道数 (如单声道、立体声)
struct AdtsHeader {
unsigned int syncword; //12 bit 同步字 '1111 1111 1111',一个ADTS帧的开始
uint8_t id; //1 bit 0代表MPEG-4, 1代表MPEG-2。
uint8_t layer; //2 bit 必须为0
uint8_t protectionAbsent; //1 bit 1代表没有CRC,0代表有CRC
uint8_t profile; //1 bit AAC级别(MPEG-2 AAC中定义了3种profile,MPEG-4 AAC中定义了6种profile)
uint8_t samplingFreqIndex; //4 bit 采样率
uint8_t privateBit; //1bit 编码时设置为0,解码时忽略
uint8_t channelCfg; //3 bit 声道数量
uint8_t originalCopy; //1bit 编码时设置为0,解码时忽略
uint8_t home; //1 bit 编码时设置为0,解码时忽略
uint8_t copyrightIdentificationBit; //1 bit 编码时设置为0,解码时忽略
uint8_t copyrightIdentificationStart; //1 bit 编码时设置为0,解码时忽略
unsigned int aacFrameLength; //13 bit 一个ADTS帧的长度包括ADTS头和AAC原始流
unsigned int adtsBufferFullness; //11 bit 缓冲区充满度,0x7FF说明是码率可变的码流,不需要此字段。CBR可能需要此字段,不同编码器使用情况不同。这个在使用音频编码的时候需要注意。
/* number_of_raw_data_blocks_in_frame
* 表示ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧
* 所以说number_of_raw_data_blocks_in_frame == 0
* 表示说ADTS帧中有一个AAC数据块并不是说没有。(一个AAC原始帧包含一段时间内1024个采样及相关数据)
*/
uint8_t numberOfRawDataBlockInFrame; //2 bit
};
解析函数分析
主要用于解析ADTS头部,从字节流中提取ADTS头部信息,然后填充到AdtsHeader结构体中
- 同步字校验: 首先检查前两个字节是否为 ADTS 同步字 (0xFFF),这是判断是否为有效 ADTS 头部的首要条件
- 位域提取: 使用位运算从 ADTS 头部字节中提取各个字段的值,例如 AAC 级别、采样率索引、声道配置、ADTS 帧长度等,并将解析出的值存储到
AdtsHeader
结构体中 - 错误处理: 如果同步字校验失败,则认为 ADTS 头部解析失败,返回错误代码
static int parseAdtsHeader(uint8_t* in, struct AdtsHeader* res) {
static int frame_number = 0;
memset(res, 0, sizeof(*res));
if ((in[0] == 0xFF) && ((in[1] & 0xF0) == 0xF0))
{
res->id = ((uint8_t)in[1] & 0x08) >> 3;//第二个字节与0x08与运算之后,获得第13位bit对应的值
res->layer = ((uint8_t)in[1] & 0x06) >> 1;//第二个字节与0x06与运算之后,右移1位,获得第14,15位两个bit对应的值
res->protectionAbsent = (uint8_t)in[1] & 0x01;
res->profile = ((uint8_t)in[2] & 0xc0) >> 6;
res->samplingFreqIndex = ((uint8_t)in[2] & 0x3c) >> 2;
res->privateBit = ((uint8_t)in[2] & 0x02) >> 1;
res->channelCfg = ((((uint8_t)in[2] & 0x01) << 2) | (((unsigned int)in[3] & 0xc0) >> 6));
res->originalCopy = ((uint8_t)in[3] & 0x20) >> 5;
res->home = ((uint8_t)in[3] & 0x10) >> 4;
res->copyrightIdentificationBit = ((uint8_t)in[3] & 0x08) >> 3;
res->copyrightIdentificationStart = (uint8_t)in[3] & 0x04 >> 2;
res->aacFrameLength = (((((unsigned int)in[3]) & 0x03) << 11) |
(((unsigned int)in[4] & 0xFF) << 3) |
((unsigned int)in[5] & 0xE0) >> 5);
res->adtsBufferFullness = (((unsigned int)in[5] & 0x1f) << 6 |
((unsigned int)in[6] & 0xfc) >> 2);
res->numberOfRawDataBlockInFrame = ((uint8_t)in[6] & 0x03);
return 0;
}
else
{
printf("failed to parse adts header\n");
return -1;
}
}
RTP AAC帧发送函数
负责将 AAC 音频帧封装成 RTP 包并通过 UDP 发送,其中AAC的RTP负载格式使用的是AU头部;整体逻辑是先设置通用的AU头部,然后将负载加入到RTP负载空间中,通过RTP发送,然后更新序列号和时间戳
代码分析
主要流程总结
- 从AAC音频文件中读取数据,然后解析ADTS的头部
- RTP数据包封装:将音频数据封装进一个RTP包中,然后通过网络进行发送
- 控制帧率,确保音频数据按照正确的速度进行播放
//开始播放,发送RTP包
if (!strcmp(method, "PLAY")) {
struct AdtsHeader adtsHeader; // 存储ADTS头部信息
struct RtpPacket* rtpPacket; // RTP包
uint8_t* frame; //存储文件中读取AAC音频数据
int ret;
FILE* fp = fopen(AAC_FILE_NAME, "rb");
if (!fp) {
printf("读取 %s 失败\n", AAC_FILE_NAME);
break;
}
frame = (uint8_t*)malloc(5000);
rtpPacket = (struct RtpPacket*)malloc(5000);
// 初始化RTP包头信息(一般是版本负载类型等)
rtpHeaderInit(rtpPacket, 0, 0, 0, RTP_VESION, RTP_PAYLOAD_TYPE_AAC, 1, 0, 0, 0x32411);
while (true)
{
// 读取前7个字节存储到frame中,也就是ADTS头部数据
ret = fread(frame, 1, 7, fp);
if (ret <= 0)
{
printf("fread err\n");
break;
}
printf("fread ret=%d \n",ret);
// 解析ADTS头部信息
if (parseAdtsHeader(frame, &adtsHeader) < 0)
{
printf("parseAdtsHeader err\n");
break;
}
// 读取完剩下的AAC音频数据
ret = fread(frame, 1, adtsHeader.aacFrameLength - 7, fp);
if (ret <= 0)
{
printf("fread err\n");
break;
}
rtpSendAACFrame(serverRtpSockfd, clientIP, clientRtpPort,
rtpPacket, frame, adtsHeader.aacFrameLength - 7);
Sleep(1);
//usleep(23223);//1000/43.06 * 1000
}
free(frame);
free(rtpPacket);
break;
}
memset(method, 0, sizeof(method) / sizeof(char));
memset(url, 0, sizeof(url) / sizeof(char));
CSeq = 0;
}
RTSP处理AAC音频SDP
将handleCmd_DESCRIBE修改为生成音频流的SDP描述
m=audio 0 RTP/AVP 97
: 媒体类型改为audio
,payload type 设置为97
(动态 payload type)。a=rtpmap:97 mpeg4-generic/44100/2
: RTP map 属性设置为mpeg4-generic
音频,采样率44100Hz
,2 声道 (立体声)。a=fmtp:97 ... config=1210;
: 关键的 FMTP 属性,包含 AAC 音频流的格式特定参数,特别是config=1210
,这是 AudioSpecificConfig (音频特定配置),以十六进制表示,解码器需要这个配置信息才能正确解码 AAC 音频。0x1210
代表 44100Hz 采样率和 2 声道
代码分析
核心流程分为三步
- 解析RTSP的URL,从URL中提取本地的IP地址
- 生成SDP数据,构建描述流媒体信息的SDP字符串
- 构建RTSP响应,将生成的SDP数据与RTSP响应拼接在一起返回给客户端
static int handleCmd_DESCRIBE(char* result, int cseq, char* url) {
char sdp[500];
char localIp[100];
// 从RTSP流地址中获取IP地址
sscanf(url, "rtsp://%[^:]:", localIp);
// 构建SDP数据
sprintf(sdp, "v=0\r\n"
"o=- 9%ld 1 IN IP4 %s\r\n"
"t=0 0\r\n"
"a=control:*\r\n"
"m=audio 0 RTP/AVP 97\r\n"
"a=rtpmap:97 mpeg4-generic/44100/2\r\n"
"a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1210;\r\n"
//"a=fmtp:97 SizeLength=13;\r\n"
"a=control:track0\r\n",
time(NULL), localIp);
sprintf(result, "RTSP/1.0 200 OK\r\nCSeq: %d\r\n"
"Content-Base: %s\r\n"
"Content-type: application/sdp\r\n"
"Content-length: %d\r\n\r\n"
"%s",
cseq,
url,
strlen(sdp),
sdp);
return 0;
}
构建SDP的内容分析
- v=0: 版本号。
- o=- 9%ld 1 IN IP4 %s: 发送者和会话标识。
time(NULL)
提供当前时间戳,localIp
则是从 URL 中提取的本地 IP 地址。 - t=0 0: 会话的开始和结束时间,这里设置为
0 0
,表示该会话没有特定的开始或结束时间。 - a=control:: 控制指令,指定
*
,表示所有的流都可以被控制。 - m=audio 0 RTP/AVP 97: 描述音频媒体流,使用 RTP 协议,流的媒体类型是音频,负载类型(Payload Type)为 97。
- a=rtpmap:97 mpeg4-generic/44100/2: 负载类型 97 的详细信息,表示使用 MPEG4 编码,采样率为 44100 Hz,声道数为 2。
- a=fmtp:97 ...: 为负载类型 97 提供的格式特定参数,指定编码方式、数据结构等详细信息。
- a=control:track0: 控制指令,指定音频轨道为
track0