关键点:
该功能实现,主要需要考虑RTSP取摄像头视频流,拆RTP包,组H264帧,通过PJSIP的视频通道转发;这个过程中,涉及到RTP通道保活,RTSP通道保活;调试时间多耗费在对摄像头返回的RTP数据包的拆解和重新组H264帧上面。
1、RTSP信令通道;
curl支持rtsp的客户端取流,demo实现也是很简单的,主要有几个点,一是用户鉴权,二是RTSP通道保活;
用户鉴权:参考 https://github.com/lminiero/rtsp-auth-test/
/* Ugly workaround needed after curl 7.66 */ curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_RTSP); curl_easy_setopt(curl, CURLOPT_HTTP09_ALLOWED, 1L); my_curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY); my_curl_easy_setopt(curl, CURLOPT_USERPWD, username_password);
信令通道保活,通过VLC抓包发现,VLC每隔8s发送一个GET_PARAMETER 消息
GET_PARAMETER rtsp://192.168.16.210/live/substream/ RTSP/1.0
CSeq: 8
User-Agent: LibVLC/2.2.1 (LIVE555 Streaming Media v2014.07.25)
Session: D8C225A1
需要解析DESCRIBE/SETUP返回的信令中的几个关键信息:profile-level-id,sprop-parameter-sets, 服务器的RTP端口
a=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=J01gNImNUFAX/LCAAAADAIAAABkHixLc,KO4PyA== Transport: RTP/AVP;unicast;client_port=6970-6971;server_port=8236-8237;ssrc=3fa5beb6;mode="play"
每隔8s发送一个Get_PARAMETER
static void rtsp_get_parmeter(CURL *curl, const char *uri) { CURLcode ret = CURLE_OK; printf("\nRTSP: rtsp_get_parmeter %s\n", uri); my_curl_easy_setopt(curl, CURLOPT_RTSP_REQUEST, CURL_RTSPREQ_GET_PARAMETER); my_curl_easy_perform(curl); return; }
碰到的问题,windows版本调试过程中,发现curl的请求socket端口在DESCRIBE信令的时候发生了变化,导致服务器返回401,要求校验用户名密码,播放失败,但linux版本不存在这个问题。
2、RTSP的媒体通道RTP/RTCP;
动态获取RTP/RTCP端口:
typedef struct { int sock; int port; struct sockaddr_in addr; } udp_t; int get_udp_port(udp_t *rtp_udp, udp_t *rtcp_udp){ static int sCurrentRTPPortToUse = 6970; static const int kMinRTPPort = 6970; static const int kMaxRTPPort = 36970; do{ int ret = udp_server_init(rtp_udp, sCurrentRTPPortToUse); if (ret == 0){ printf("get_udp_port rtp:%d\r\n", rtp_udp->port); break; } sCurrentRTPPortToUse++; if (sCurrentRTPPortToUse == kMaxRTPPort){ printf("sCurrentRTPPortToUse is:%d\r\n", sCurrentRTPPortToUse); return -1; } }while(1); sCurrentRTPPortToUse++; do{ int ret = udp_server_init(rtcp_udp, sCurrentRTPPortToUse); if (ret == 0){ printf("get_udp_port rtcp:%d\r\n", rtcp_udp->port); break; } sCurrentRTPPortToUse++; if (sCurrentRTPPortToUse == kMaxRTPPort){ printf("sCurrentRTPPortToUse is:%d\r\n", sCurrentRTPPortToUse); if (rtp_udp->port != 0){ udp_server_deinit(rtp_udp); } return -1; } }while(1); return 0; }
媒体通道保活
static void send_stun_packet(int sock, char *dst_ip, int dst_port){ struct sockaddr_in b_addr; b_addr.sin_family=AF_INET; b_addr.sin_addr.s_addr=inet_addr(dst_ip); b_addr.sin_port=htons(dst_port); int b_addr_len=sizeof(b_addr); uint8_t temp_code[] = {0xce,0xfe, 0xed, 0xfe}; printf("send_stun_packet,dst_ip:%s, port:%d.\r\n", dst_ip, dst_port); int send_len = sendto(sock, temp_code, sizeof(temp_code), 0,(struct sockaddr *)&b_addr, b_addr_len); if (send_len < 0) { printf("\n\rsend error.\n\r"); } return; }
RTP重新组包:
关键的点,就是将H264的RTP封包重新组成完整的H264帧,每一个不同的帧类型包括如下,每个帧类型前面都要补充0x00,0x00,0x00,0x01,否则不能解码。
0x67: SPS 0x68: PPS 0x65: IDR 0x61: non-IDR Slice 0x01: B Slice 0x06: SEI 0x09: AU Delimiter
void put_frame(uint8_t *data, int len){ uint8_t sync_bytes[] = {0, 0, 0, 1}; if (data == NULL){ return; } int seq = (data[2] << 8) | data[3]; int pt = data[1]&0x7f; int marker = ((data[1]&0x80) >> 7); uint8_t *payload = &data[12]; data += 12; len -=12; uint8_t nalu_hdr = *data; int nalu_type = data[0] & 0x1f; int old_type = nalu_type; if (last_rtp_frame_cache_len == 0){ printf_data(data, 7); printf("[1]seq:%d, old_type:%d, nalu_type:%d marker:%d, len:%d,last_len:%d\r\n", seq,old_type, nalu_type, marker, len, last_rtp_frame_cache_len); } if (nalu_type == 28) { // 0x1c FU-A int start = (*(data + 1) & 0x80); int end = (*(data + 1) & 0x40); nalu_type = (*(data + 1) & 0x1f); if (start){ uint8_t nalu_idc = (nalu_hdr & 0x60) >> 5; //printf_data(data, 7); nalu_type |= (nalu_idc << 5); memcpy(rtp_frame_cache+last_rtp_frame_cache_len, sync_bytes, 4); last_rtp_frame_cache_len += 4; memcpy(rtp_frame_cache +last_rtp_frame_cache_len, &nalu_type, 1); last_rtp_frame_cache_len += 1; printf_data(rtp_frame_cache, last_rtp_frame_cache_len); } len -= 2; payload += 2; printf("nalu_type:%2x,start:%d, end:%d,last_rtp_frame_cache_len:%d \r\n", nalu_type, start, end,last_rtp_frame_cache_len); if (last_rtp_frame_cache_len + len >= rtp_frame_cache_max_len){ rtp_frame_cache = (uint8_t *)realloc(rtp_frame_cache, 10240); rtp_frame_cache_max_len += 10240; } //拷贝payload memcpy((void *)(rtp_frame_cache+last_rtp_frame_cache_len), payload, len); last_rtp_frame_cache_len += len; }else if (nalu_type == 24) { // 0x18 STAP-A nalu_type = *(data + 1) & 0x1f; len -= 3; payload += 3; nalu_hdr = *data; nalu_type = nalu_hdr & 0x1f; printf("[2]nalu_type:%2x, \r\n"); memcpy(rtp_frame_cache+last_rtp_frame_cache_len, sync_bytes, 4); last_rtp_frame_cache_len += 4; if (last_rtp_frame_cache_len + len >= rtp_frame_cache_max_len){ rtp_frame_cache = (uint8_t *)realloc(rtp_frame_cache, 10240); rtp_frame_cache_max_len += 10240; } //拷贝payload memcpy((void *)(rtp_frame_cache+last_rtp_frame_cache_len), payload, len); last_rtp_frame_cache_len += len; } else { int pps_pos = 0; printf("[2]seq:%d, data:%02x, nalu_type:%d marker:%d, len:%d,last_len:%d\r\n", seq,data[0], nalu_type, marker, len, last_rtp_frame_cache_len); if (nalu_type == 7||nalu_type == 8){ memcpy((void *)(rtp_frame_cache+last_rtp_frame_cache_len), sync_bytes, 4); last_rtp_frame_cache_len += 4; //找0x68.补上header if (nalu_type == 7){ memcpy((void *)(rtp_frame_cache+last_rtp_frame_cache_len), rtsp_server_sps, rtsp_server_sps_len); last_rtp_frame_cache_len += rtsp_server_sps_len; if (len >= (rtsp_server_sps_len + rtsp_server_pps_len)){ memcpy((void *)(rtp_frame_cache+last_rtp_frame_cache_len), sync_bytes, 4); last_rtp_frame_cache_len += 4; memcpy((void *)(rtp_frame_cache+last_rtp_frame_cache_len), rtsp_server_pps, rtsp_server_pps_len); last_rtp_frame_cache_len += rtsp_server_pps_len; int reserved_len = len - rtsp_server_sps_len - rtsp_server_pps_len; if (reserved_len > 0){ payload += rtsp_server_pps_len + rtsp_server_sps_len; printf("[2]reserved_len:%2x, \r\n"); memcpy((void *)(rtp_frame_cache+last_rtp_frame_cache_len), payload, reserved_len); last_rtp_frame_cache_len += reserved_len; } } }else { memcpy((void *)(rtp_frame_cache+last_rtp_frame_cache_len), rtsp_server_pps, rtsp_server_pps_len); last_rtp_frame_cache_len += rtsp_server_pps_len; int reserved_len = len - rtsp_server_pps_len; if (reserved_len > 0){ payload += rtsp_server_pps_len + rtsp_server_sps_len; printf("[2]reserved_len:%2x, \r\n"); memcpy((void *)(rtp_frame_cache+last_rtp_frame_cache_len), payload, reserved_len); last_rtp_frame_cache_len += reserved_len; } } }else{ if (last_rtp_frame_cache_len == 0){ memcpy((void *)(rtp_frame_cache+last_rtp_frame_cache_len), sync_bytes, 4); last_rtp_frame_cache_len += 4; } if (last_rtp_frame_cache_len + len >= rtp_frame_cache_max_len){ rtp_frame_cache = (uint8_t *)realloc(rtp_frame_cache, 10240); rtp_frame_cache_max_len += 10240; } //拷贝payload memcpy((void *)(rtp_frame_cache+last_rtp_frame_cache_len), payload, len); last_rtp_frame_cache_len += len; } } if (marker){ //reset 0 if (getFrameCallback != NULL){ printf("last_rtp_frame_cache_len:%d\r\n", last_rtp_frame_cache_len); getFrameCallback(rtp_frame_cache, last_rtp_frame_cache_len, frameCallbackArgs); } last_rtp_frame_cache_len = 0; } }
3、参考增加一个虚拟的video设备;
参考:https://it3q.com/article/185
4、开放的接口;
rtsp_client部分的接口,
typedef struct pjmedia_rtsp_source_op { int (*init_rtsp_client)(); int (*deinit_rtsp_client)(); int (*start_rtsp_client)(const char *url, OnGetFrameFromRTSP callback, void *user_data); int (*stop_rtsp_client)(); }pjmedia_rtsp_source_op; extern void set_use_rtsp_source(const char *url, pjmedia_rtsp_source_op *op); int start_rtsp_client_sip(const char *url, OnGetFrameFromRTSP callback, void *user_data); static pjmedia_rtsp_source_op factory_op = { &init_rtsp_client, &deinit_rtsp_client, &start_rtsp_client_sip, &stop_rtsp_client };
pjsip的接口:
void register_rtsp_client_source(const char *url){ if (url == NULL){ return; } printf("call register url:%s\r\n", url); set_use_rtsp_source(url, &factory_op); }
5、注意:rtsp协商的h264编码参数,需要同步到pjsip中,建议是先通过抓包,取的某个型号摄像头的参数后,配置到pjsip中。
主要包括:profile-level-id=420029; packetization-mode=1;
代码目录结构:
交叉编译,拷贝过来的交叉编译器,需要调整sysroot,否则gcc报错。
root@lyz-VirtualBox:/home/lyz/work/broadcast_app/v3s_ipc_rtsp_pjsip/curl-8.2.1# arm-buildroot-linux-uclibcgnueabihf-gcc -v Using built-in specs. COLLECT_GCC=/home/lyz/work/broadcast_app/v3s_ipc_rtsp_pjsip/buildroot-2018.08.2/output/host/bin/arm-buildroot-linux-uclibcgnueabihf-gcc.br_real COLLECT_LTO_WRAPPER=/home/lyz/work/broadcast_app/v3s_ipc_rtsp_pjsip/buildroot-2018.08.2/output/host/bin/../libexec/gcc/arm-buildroot-linux-uclibcgnueabihf/7.3.0/lto-wrapper Target: arm-buildroot-linux-uclibcgnueabihf Configured with: ./configure --prefix=/home/psst/v3s/buildroot-2018.08.2/output/host --sysconfdir=/home/psst/v3s/buildroot-2018.08.2/output/host/etc --enable-static --target=arm-buildroot-linux-uclibcgnueabihf --with-sysroot=/home/psst/v3s/buildroot-2018.08.2/output/host/arm-buildroot-linux-uclibcgnueabihf/sysroot --disable-__cxa_atexit --with-gnu-ld --disable-libssp --disable-multilib --with-gmp=/home/psst/v3s/buildroot-2018.08.2/output/host --with-mpc=/home/psst/v3s/buildroot-2018.08.2/output/host --with-mpfr=/home/psst/v3s/buildroot-2018.08.2/output/host --with-pkgversion='Buildroot 2018.08.2' --with-bugurl=http://bugs.buildroot.net/ --disable-libquadmath --disable-libsanitizer --enable-tls --disable-libmudflap --enable-threads --without-isl --without-cloog --disable-decimal-float --with-abi=aapcs-linux --with-cpu=cortex-a7 --with-fpu=vfpv4 --with-float=hard --with-mode=arm --enable-languages=c,c++ --with-build-time-tools=/home/psst/v3s/buildroot-2018.08.2/output/host/arm-buildroot-linux-uclibcgnueabihf/bin --enable-shared --disable-libgomp Thread model: posix gcc version 7.3.0 (Buildroot 2018.08.2)
-------------------广告线---------------
项目、合作,欢迎勾搭,邮箱:promall@qq.com
本文为呱牛笔记原创文章,转载无需和我联系,但请注明来自呱牛笔记 ,it3q.com