概述
实现一个简单的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 必须为0uint8_t protectionAbsent; //1 bit 1代表没有CRC,0代表有CRCuint8_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