【RTSP】客户端(二):SDP解析

sdp基础

项目中的实例

v=0
o=34020000001320000001 0 0 IN IP4 121.229.21.99
s=Play
c=IN IP4 121.229.21.99
t=0 0
m=video 30196 RTP/AVP 96 97 98
a=recvonly
a=rtpmap:96 PS/90000
a=rtpmap:97 MPEG4/90000
a=rtpmap:98 H264/90000
y=0200000001
  • v=0
    • v:协议版本号,这里是 0,表示 SDP 协议的版本
  • o=34020000001320000001 0 0 IN IP4 121.229.21.99
    • 会话的创建者也就是摄像头,后面的IP地址是SIP服务器的地址
    • o:会话的创建者和会话标识符,34020000001320000001 是创建会话的设备,后面的 0 0 表示版本号和编号,IN IP4 121.229.21.99 表示会话地址类型和使用的 IP 地址
  • s=Play
    • s:会话名称,这里是play,也就是一个播放会话
  • c=IN IP4 121.229.21.99
    • 表示连接信息,也就是连接SIP服务端的地址信息
  • t=0 0
    • t:会话的时间范围,这里是 0 0,表示会话没有特定的开始和结束时间,通常用于实时流媒体
  • m=video 30196 RTP/AVP 96 97 98
    • m:媒体描述,表示该会话有一个视频流,端口号是 30196,使用 RTP/AVP 协议,支持的媒体类型为 96(PS)、97(MPEG4)、98(H264)
  • a=recvonly
    • 会话属性,recvonly 表示该媒体流是接收-only(只接收数据)
  • a=rtpmap:96 PS/90000
    • a=rtpmap:描述媒体流的编码格式,96 代表 PS(播放顺序),90000 是该媒体流的时钟频率
  • a=rtpmap:97 MPEG4/90000
    • 同上,97 代表 MPEG4 编码格式
  • a=rtpmap:98 H264/90000
    • 同上,98 代表 H264 编码格式。
  • y=0200000001
    • y:这个字段可能是某种设备或会话标识符,在这个上下文中,它看起来像是某种设备编号或会话 ID

代码实现

常见完整事例

v=0
o=- 91720590340 1 IN IP4 192.168.10.17
c=IN IP4 192.168.10.17
t=0 0
a=control:*
m=video 0 RTP/AVP 96
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1
a=control:track0
m=audio 0 RTP/AVP 97
a=rtpmap:97 MPEG4-GENERIC/44100/2
a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;config=1390;sizelength=13;indexlength=3;indexdeltalength=3
a=control:track1

使用金MediaInfo结构体存储SDP的信息

struct MediaInfo {
    std::string media_name; // video / audio
    MediaEnum media_type;   // 媒体类型
    std::string contorl;    // track0 track1 or url/track0 url/track1

    int payload;            // rtp payload,表示RTP payload的类型
    int sample_rate;        // only audio
    int sample_rate_index;  // only audio
    int channels;           // only audio
    int profile;            // only audio from config=1390
};

视频部分存储内容

  • media_name: "video"
  • media_type: Video(假设 MediaEnum 中有 Video
  • control: "track0"
  • payload: 96

音频存储内容

  • media_name: "audio"
  • media_type: Audio(假设 MediaEnum 中有 Audio
  • control: "track1"
  • payload: 97
  • sample_rate: 44100(从 a=rtpmap:97 MPEG4-GENERIC/44100/2 中提取)
  • sample_rate_index: 这个值需要根据音频协议或约定来推断(此例没有明确标识)
  • channels: 2(从 a=rtpmap:97 MPEG4-GENERIC/44100/2 中提取)
  • profile: 1390(从 a=fmtp:97 config=1390 中提取)

存储SDP会话相关信息

struct SdpInfo {
    std::string contorl;            // * or url
    struct MediaInfo media_info[2]; // 0-video 1-audio
    int media_count;
};
  • std::string contorl
    • 媒体控制信息,一般是通过a=control字段指定的,例如上述例子中是*表示所有媒体共享控制通道,也可以设置为特定的URL路径
  • struct MediaInfo media_info[2]
    • 包含两个存储MediaInfo结构体的数组,分别用于存储视频和音频相关的信息
    • 其中第一个元素存储视频信息;第二个元素存储音频信息
  • int media_count
    • 记录当前SDP会话中包含的媒体数量,一般情况下,如果会话中有视频和音频,那么media_count就会被设置为2
    • 如果只有视频的话,那么这里的字段就只会对应的设置为1

初始化

根据传输的SDP数据和基础的URL来解析SDP内容

SDPParse::SDPParse(std::string sdp, std::string base_url){
    // 1. 保存sdp和base_url
    sdp_ = sdp;
    base_url_ = base_url;
    // 2. 提取会话信息和媒体信息
    int pos = sdp_.find("m=");
    if(pos != std::string::npos){
        sdp_session_ = sdp_.substr(0, pos);
    }
    else{
        sdp_session_ = sdp_;
    }
    while ((pos = sdp_.find("m=", pos)) != std::string::npos) {
        size_t next_pos = sdp_.find("m=", pos + 1);
        std::string media_block;
        if (next_pos != std::string::npos) {
            media_block = sdp_.substr(pos, next_pos - pos);
        } else {
            media_block = sdp_.substr(pos);
        }
        media_descriptions_.push_back(media_block);
        if(media_block.find("video") != std::string::npos){
            sdp_video_ = media_block;
        }
        if(media_block.find("audio") != std::string::npos){
            sdp_audio_ = media_block;
        }
        pos = next_pos;
    }
}

获取视频控制的URL

// 获取视频的URL,根据解析出来的 sdp_info_ 中的视频控制信息,结合基础 URL (base_url_),生成并返回视频的 URL
std::string SDPParse::GetVideoUrl(){
    // 检查是否存在视频媒体信息
    if(sdp_info_.media_info[0].media_type !=  MediaEnum::NONE){
        // 如果会话控制信息为 *,则直接返回基础 URL (base_url_) 加上视频控制信息
        if(sdp_info_.contorl == std::string("*")){
            std::string url;
            char ch = base_url_[base_url_.size()-1];
            if(ch != '/'){
                url = base_url_ + std::string("/");
            }
            else{
                url = base_url_;
            }
            return url + sdp_info_.media_info[0].contorl;
        }
        else{
            // session a=control:rtsp://192.168.0.63/media/video1/
            // meida a=control:rtsp://192.168.0.63/media/video1/trackID=1
            if((sdp_info_.media_info[0].contorl.size() >= sdp_info_.contorl.size()) && 
                (memcmp(sdp_info_.contorl.c_str(), sdp_info_.media_info[0].contorl.c_str(), sdp_info_.contorl.size()) == 0)){
                return sdp_info_.media_info[0].contorl;
            }
            // session a=control:rtsp://192.168.0.63/media/video1/
            // meida a=trackID=1
            std::string url;
            char ch = sdp_info_.contorl[sdp_info_.contorl.size()-1];
            if(ch != '/'){
                url = sdp_info_.contorl + std::string("/");
            }
            else{
                url = sdp_info_.contorl;
            }
            return url + sdp_info_.media_info[0].contorl;
        }
    }
    return "";
}

测试代码 

    void TestGetVideoUrl() {
        // Test case 1: Control is "*"
        SDPParse sdp_parser("*", "rtsp://192.168.0.63/media");
        std::string video_url = sdp_parser.GetVideoUrl();
        std::cout << "Test case 1: " << video_url << std::endl;  // Expected: rtsp://192.168.0.63/media/track0
    
        // Test case 2: Control is not "*"
        sdp_parser.sdp_info_.contorl = "rtsp://192.168.0.63/media";
        video_url = sdp_parser.GetVideoUrl();
        std::cout << "Test case 2: " << video_url << std::endl;  // Expected: rtsp://192.168.0.63/media/track0
    
        // Test case 3: No video media stream
        sdp_parser.sdp_info_.media_info[0].media_type = MediaEnum::NONE;
        video_url = sdp_parser.GetVideoUrl();
        std::cout << "Test case 3: " << (video_url.empty() ? "No video URL" : video_url) << std::endl;  // Expected: No video URL
    }

获取音频控制的URL

// 获取音频的控制URL
std::string SDPParse::GetAudioUrl(){
    if(sdp_info_.media_info[1].media_type !=  MediaEnum::NONE){
        if(sdp_info_.contorl == std::string("*")){
            std::string url;
            char ch = base_url_[base_url_.size()-1];
            if(ch != '/'){
                url = base_url_ + std::string("/");
            }
            else{
                url = base_url_;
            }
            return url + sdp_info_.media_info[1].contorl;
        }
        else{
            // session a=control:rtsp://192.168.0.63/media/video1/
            // meida a=control:rtsp://192.168.0.63/media/video1/trackID=1
            if((sdp_info_.media_info[1].contorl.size() >= sdp_info_.contorl.size()) && 
                (memcmp(sdp_info_.contorl.c_str(), sdp_info_.media_info[1].contorl.c_str(), sdp_info_.contorl.size()) == 0)){
                return sdp_info_.media_info[1].contorl;
            }
            // session a=control:rtsp://192.168.0.63/media/video1/
            // meida a=trackID=1
            std::string url;
            char ch = sdp_info_.contorl[sdp_info_.contorl.size()-1];
            if(ch != '/'){
                url = sdp_info_.contorl + std::string("/");
            }
            else{
                url = sdp_info_.contorl;
            }
            return url + sdp_info_.media_info[1].contorl;
        }
    }
    return "";
}

解析SDP会话信息中的控制字段

解析控制字段后存储到sdp_info的contorl中(也就是前面初始化的结构体)

/*
v=0
o=- 91720590340 1 IN IP4 192.168.10.17
c=IN IP4 192.168.10.17
t=0 0
a=control:*
*/
int SDPParse::ParseSession(){
    int start = sdp_session_.find("a=control:");
    int end = sdp_session_.find("\r\n", start);
    sdp_info_.contorl = sdp_session_.substr(start + strlen("a=control:"), end - start - strlen("a=control:"));
    return 0;
}
static std::vector<std::string> SplitString(const std::string& str){
    std::vector<std::string> result;
    std::istringstream iss(str);
    std::string word;
    while (iss >> word) {
        result.push_back(word);
    }
    return result;
}

解析SDP中与视频相关的信息

/*
sdp_video_:m=video 0 RTP/AVP 96
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1
a=control:track0
*/
int SDPParse::ParseVideo(){
    // 1. 判断是否有视频描述信息
    if(sdp_video_.empty()){
        return 0;
    }
    sdp_info_.media_count++;
    // 2. 设置视频体的名称
    sdp_info_.media_info[0].media_name = "video";

    // 3. 提取RTP的负载类型
    int start = sdp_video_.find("m=video ");
    int end = sdp_video_.find("\r\n");
    std::string m_line = sdp_video_.substr(start + strlen("m=video "), end - start - strlen("m=video "));
    std::vector<std::string> res = SplitString(m_line);
    sdp_info_.media_info[0].payload = atoi(res[2].c_str()); // 选取第一个payload

    // 4. 解析视频控制信息
    start = sdp_video_.find("a=control:");
    end = sdp_video_.find("\r\n", start);
    sdp_info_.media_info[0].contorl = sdp_video_.substr(start + strlen("a=control:"), end - start - strlen("a=control:"));

    // 5. 解析rtpmap和fmtp
    std::string rtpmap = std::string("a=rtpmap:") + std::to_string(sdp_info_.media_info[0].payload);
    std::string fmtp = std::string("a=fmtp:") + std::to_string(sdp_info_.media_info[0].payload);
    // 6. 获取编码样式和采样率
    start = sdp_video_.find(rtpmap.c_str());
    end = sdp_video_.find("\r\n", start);
    std::string rtpmap_line = sdp_video_.substr(start + rtpmap.size(), end - start - rtpmap.size());

    char buffer[512] = {0};
    int sample_rate;
    sscanf(rtpmap_line.c_str(), " %[^/]/%d", buffer, &sample_rate);
    sdp_info_.media_info[0].sample_rate = sample_rate;
    // 7. 设置视频的编码格式
    if(std::string(buffer) == std::string("H264")){
        sdp_info_.media_info[0].media_type = MediaEnum::H264;
    }
    else if((std::string(buffer) == std::string("H265")) || (std::string(buffer) == std::string("HEVC"))){
        sdp_info_.media_info[0].media_type = MediaEnum::H265;
    }
    else{
        std::cout << "only support H264/H265, but sdp media type:" << buffer << std::endl;
        return -1;
    }
    // fmtp

    sdp_info_.media_info[0].channels = 0;
    sdp_info_.media_info[0].profile = 0;
    return 0;
}

解析SDP中与音频流相关的信息

测试函数

#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <sstream>

enum class MediaEnum {
    NONE,
    VIDEO,
    AUDIO,
    H264,
    H265,
    AAC,
    PCMA
};

struct MediaInfo {
    std::string media_name;   // "video" or "audio"
    MediaEnum media_type;     // e.g., H264, H265, AAC, PCMA
    std::string contorl;      // control info (e.g., track0, track1)
    int payload;              // RTP payload type
    int sample_rate;          // audio sample rate (for audio)
    int channels;             // audio channels (for audio)
    int profile;              // audio profile (for audio)
    int sample_rate_index;    // audio sample rate index (for audio)
};

struct SdpInfo {
    int media_count;           // Number of media streams (audio/video)
    MediaInfo media_info[2];   // [0] for video, [1] for audio
    std::string contorl;       // global control (e.g., "*")
};

class SDPParse {
public:
    std::string sdp_video_;
    std::string sdp_audio_;
    SdpInfo sdp_info_;

    // Parse video information
    int ParseVideo() {
        if (sdp_video_.empty()) {
            return 0;
        }
        sdp_info_.media_count++;
        sdp_info_.media_info[0].media_name = "video";
        int start = sdp_video_.find("m=video ");
        int end = sdp_video_.find("\r\n");
        std::string m_line = sdp_video_.substr(start + strlen("m=video "), end - start - strlen("m=video "));
        std::vector<std::string> res = SplitString(m_line);
        sdp_info_.media_info[0].payload = atoi(res[2].c_str());  // First payload

        start = sdp_video_.find("a=control:");
        end = sdp_video_.find("\r\n", start);
        sdp_info_.media_info[0].contorl = sdp_video_.substr(start + strlen("a=control:"), end - start - strlen("a=control:"));

        std::string rtpmap = "a=rtpmap:" + std::to_string(sdp_info_.media_info[0].payload);
        start = sdp_video_.find(rtpmap.c_str());
        end = sdp_video_.find("\r\n", start);
        std::string rtpmap_line = sdp_video_.substr(start + rtpmap.size(), end - start - rtpmap.size());

        char buffer[512] = {0};
        int sample_rate;
        sscanf(rtpmap_line.c_str(), " %[^/]/%d", buffer, &sample_rate);
        sdp_info_.media_info[0].sample_rate = sample_rate;
        
        if (std::string(buffer) == "H264") {
            sdp_info_.media_info[0].media_type = MediaEnum::H264;
        } else if (std::string(buffer) == "H265") {
            sdp_info_.media_info[0].media_type = MediaEnum::H265;
        } else {
            std::cout << "Unsupported video format: " << buffer << std::endl;
            return -1;
        }

        return 0;
    }

    // Parse audio information
    int ParseAudio() {
        if (sdp_audio_.empty()) {
            return 0;
        }
        sdp_info_.media_count++;
        sdp_info_.media_info[1].media_name = "audio";
        int start = sdp_audio_.find("m=audio ");
        int end = sdp_audio_.find("\r\n");
        std::string m_line = sdp_audio_.substr(start + strlen("m=audio "), end - start - strlen("m=audio "));
        std::vector<std::string> res = SplitString(m_line);
        sdp_info_.media_info[1].payload = atoi(res[2].c_str());  // First payload

        start = sdp_audio_.find("a=control:");
        end = sdp_audio_.find("\r\n", start);
        sdp_info_.media_info[1].contorl = sdp_audio_.substr(start + strlen("a=control:"), end - start - strlen("a=control:"));

        std::string rtpmap = "a=rtpmap:" + std::to_string(sdp_info_.media_info[1].payload);
        start = sdp_audio_.find(rtpmap.c_str());
        end = sdp_audio_.find("\r\n", start);
        std::string rtpmap_line = sdp_audio_.substr(start + rtpmap.size(), end - start - rtpmap.size());

        char buffer[512] = {0};
        int sample_rate, channels;
        sscanf(rtpmap_line.c_str(), " %[^/]/%d/%d", buffer, &sample_rate, &channels);
        sdp_info_.media_info[1].sample_rate = sample_rate;
        sdp_info_.media_info[1].channels = channels;

        if (std::string(buffer) == "MPEG4-GENERIC") {
            sdp_info_.media_info[1].media_type = MediaEnum::AAC;
        } else if (std::string(buffer) == "PCMA") {
            sdp_info_.media_info[1].media_type = MediaEnum::PCMA;
        } else {
            std::cout << "Unsupported audio format: " << buffer << std::endl;
            return -1;
        }

        return 0;
    }

private:
    // Helper function to split a string by spaces
    std::vector<std::string> SplitString(const std::string& str) {
        std::vector<std::string> result;
        std::istringstream iss(str);
        std::string word;
        while (iss >> word) {
            result.push_back(word);
        }
        return result;
    }
};

// Test function
void TestSDPParse() {
    SDPParse sdp_parser;

    // Example video SDP string
    std::string video_sdp = 
        "m=video 0 RTP/AVP 96\r\n"
        "a=rtpmap:96 H264/90000\r\n"
        "a=fmtp:96 packetization-mode=1\r\n"
        "a=control:track0\r\n";
    sdp_parser.sdp_video_ = video_sdp;
    
    // Example audio SDP string
    std::string audio_sdp = 
        "m=audio 0 RTP/AVP 97\r\n"
        "a=rtpmap:97 MPEG4-GENERIC/44100/2\r\n"
        "a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;config=1390;\r\n"
        "a=control:track1\r\n";
    sdp_parser.sdp_audio_ = audio_sdp;

    // Parse video and audio
    sdp_parser.ParseVideo();
    sdp_parser.ParseAudio();

    // Check results for video
    std::cout << "Video Media Name: " << sdp_parser.sdp_info_.media_info[0].media_name << std::endl;
    std::cout << "Video Payload: " << sdp_parser.sdp_info_.media_info[0].payload << std::endl;
    std::cout << "Video Control: " << sdp_parser.sdp_info_.media_info[0].contorl << std::endl;
    std::cout << "Video Sample Rate: " << sdp_parser.sdp_info_.media_info[0].sample_rate << std::endl;
    std::cout << "Video Media Type: " << (sdp_parser.sdp_info_.media_info[0].media_type == MediaEnum::H264 ? "H264" : "Other") << std::endl;

    // Check results for audio
    std::cout << "Audio Media Name: " << sdp_parser.sdp_info_.media_info[1].media_name << std::endl;
    std::cout << "Audio Payload: " << sdp_parser.sdp_info_.media_info[1].payload << std::endl;
    std::cout << "Audio Control: " << sdp_parser.sdp_info_.media_info[1].contorl << std::endl;
    std::cout << "Audio Sample Rate: " << sdp_parser.sdp_info_.media_info[1].sample_rate << std::endl;
    std::cout << "Audio Channels: " << sdp_parser.sdp_info_.media_info[1].channels << std::endl;
    std::cout << "Audio Media Type: " << (sdp_parser.sdp_info_.media_info[1].media_type == MediaEnum::AAC ? "AAC" : "Other") << std::endl;
}

int main() {
    TestSDPParse();
    return 0;
}
/*
m=audio 0 RTP/AVP 97
a=rtpmap:97 MPEG4-GENERIC/44100/2
a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;config=1390;sizelength=13;indexlength=3;indexdeltalength=3
a=control:track1
*/
int SDPParse::ParseAudio(){
    // 1. 判断音频是否存在
    if(sdp_audio_.empty()){
        return 0;
    }
    sdp_info_.media_count++;
    // 2. 设置音频的名称
    sdp_info_.media_info[1].media_name = "audio";
    // 3. 提取RTP的负载类型
    int start = sdp_audio_.find("m=audio ");
    int end = sdp_audio_.find("\r\n");
    std::string m_line = sdp_audio_.substr(start + strlen("m=audio "), end - start - strlen("m=audio "));
    std::vector<std::string> res = SplitString(m_line);
    sdp_info_.media_info[1].payload = atoi(res[2].c_str()); // 选取第一个payload

    // 4. 解析音频控制信息
    start = sdp_audio_.find("a=control:");
    end = sdp_audio_.find("\r\n", start);
    sdp_info_.media_info[1].contorl = sdp_audio_.substr(start + strlen("a=control:"), end - start - strlen("a=control:"));

    // 5. 解析rtpmap和fmtp
    std::string rtpmap = std::string("a=rtpmap:") + std::to_string(sdp_info_.media_info[1].payload);
    std::string fmtp = std::string("a=fmtp:") + std::to_string(sdp_info_.media_info[1].payload);
    // rtpmap
    start = sdp_audio_.find(rtpmap.c_str());
    end = sdp_audio_.find("\r\n", start);
    std::string rtpmap_line = sdp_audio_.substr(start + rtpmap.size(), end - start - rtpmap.size());

    char buffer[512] = {0};
    int sample_rate;
    int channels;
    sscanf(rtpmap_line.c_str(), " %[^/]/%d/%d", buffer, &sample_rate, &channels);
    sdp_info_.media_info[1].sample_rate = sample_rate;
    sdp_info_.media_info[1].channels = channels;
    
    // 6. 设置音频的编码格式 
    if((std::string(buffer) == std::string("MPEG4-GENERIC")) || (std::string(buffer) == std::string("mpeg4-generic"))){
        sdp_info_.media_info[1].media_type = MediaEnum::AAC;
    }
    else if((std::string(buffer) == std::string("PCMA")) || (std::string(buffer) == std::string("pcma"))){
        sdp_info_.media_info[1].media_type = MediaEnum::PCMA;
    }
    else{
        std::cout << "only support AAC(MPEG4-GENERIC)/PCMA, but sdp media type:" << buffer << std::endl;
        return -1;
    }
    // fmtp
    start = sdp_audio_.find(fmtp.c_str());
    end = sdp_audio_.find("\r\n", start);
    std::string fmtp_line = sdp_audio_.substr(start + fmtp.size(), end - start - fmtp.size());

    if(fmtp_line.find("config=") != std::string::npos){
        std::string config_value;
        int pos1 =  fmtp_line.find("config=");
        int pos2 = fmtp_line.find(';', pos1);
        if(pos2 != std::string::npos){
            config_value = fmtp_line.substr(pos1 + strlen("config="), pos2 - pos1 -strlen("config="));
        }
        else{
            config_value = fmtp_line.substr(pos1 + strlen("config="));
        }
        // config_value - 16进制
        long int  config = (uint16_t)strtol(config_value.c_str(), NULL, 16);
        int profile_config = (config >> 11) & 0x1f;
        int sample_rate_index_config = (config >> 7) & 0x0f;
        int channels_config = (config >> 3) & 0x0f;
        // std::cout << "profile_config:" << profile_config << "sample_rate_index_config:" << sample_rate_index_config << "channels_config:" << channels_config << std::endl;
        // 如果有config,则以config中的配置为准
        sdp_info_.media_info[1].profile = profile_config;
        sdp_info_.media_info[1].sample_rate_index = sample_rate_index_config;
        sdp_info_.media_info[1].channels = channels_config;
        int freq_arr[13] = {
            96000, 88200, 64000, 48000, 44100, 32000,
            24000, 22050, 16000, 12000, 11025, 8000, 7350
        };
        sdp_info_.media_info[1].sample_rate = freq_arr[sample_rate_index_config];
    }
    else{
        sdp_info_.media_info[1].profile = 1;
        sdp_info_.media_info[1].sample_rate_index = GetSampleRateIndex(sdp_info_.media_info[1].sample_rate);
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值