2025.06.27-14.44 C语言开发:Onvif(二)
认证
SOAP报文认证
Onvif支持在SOAP报文里进行认证,即在XML结构里,加入用户名密码。
在gsoap的实现里,使用wsse插件中的 soap_wsse_add_UsernameTokenDigest
即可。
这个函数的原型为:
SOAP_FMAC1 int SOAP_FMAC2 soap_wsse_add_UsernameTokenDigest(struct soap *soap, const char *id, const char *username, const char *password);
使用的时候,在需要认证的url执行方法之前,调用这个函数加入用户名密码即可实现SOAP报文的认证。
HTTP认证
因为Onvif的Server就是Web Server,所以还有的Onvif服务,光在SOAP报文里认证不够,需要HTTP层的认证。
HTTP层的认证方式包括:要么Basic,要么Digest。
在gsoap的插件里,相应的实现是http_da。
大概分成几步:
- soap客户端请求。
- 服务端返回错误码401,以及鉴权码。
- soap客户端根据鉴权吗,加入认证信息,再次请求。
- 服务端认证通过。
示例如下:
// 下面的func、req、response为实际请求的方法、请求结构体指针以及响应结构体指针
int
soap_auth (struct gsoap *soap, const char *uri, const char *username,const char *password)
{int res;struct http_da_info hdinfo[1];// 1. 第一次提交请求,如果不出错,表示不需要认证,直接返回res = func (soap, uri, NULL, req, response);if (res == 0){return 0;}// 2. 如果返回状态码不是401,表示不是认证的问题,直接返回-1if (soap->status != 401){fprintf (stderr, "soap need auth: %d, %d, %s\n", soap->error, *soap_faultcode (soap), *soap_faultstring (soap));return -1;}// 3. 设置认证信息,包括服务端反正的authrealm,以及用户名、密码http_da_save (soap, hdinfo, soap->authrealm, username, password);// 再次提交请求res = func (soap, uri, NULL, req, response);http_da_release (soap, hdinfo);// 4. 如果返回0,表示认证成功if (res == 0){return 0;}// 认证失败fprintf (stderr, "soap auth error: %d, %s, %s\n", soap->error, *soap_faultcode (soap), *soap_faultstring (soap));return -1;
}
需要注意的是,有的摄像机的HTTP服务端,实现的认证过程会多一次。
即,第一次返回401以后,客户端加入realm、用户名、密码,再次提交,再返回401,这时候客户端再次使用返回的realm,以及自己的用户名、密码,进行提交,才会认证通过。
Onvif摄像机点播
Onvif 的摄像机点播的时候,可以分成这样几步:
- 取得摄像机的Capabilities
- 取得摄像机的Profiles
- 取得相应Profile的StreamUri
取得Capabilites
取得Capabilities的函数为soap_call___tds__GetCapabilities
,输入参数分别为struct _tds__GetCapabilities *
和struct _tds__GetCapabilitiesResponse *
。
struct _tds__GetCapabilitiesResponse
的结构里有一个Capabilities结构,这个结构的Media就表示媒体信息。
gchar *
soap_getcapabilities (struct gsoap* soap, char M_XAddr[], size_t M_XSize)
{ struct _tds__GetCapabilities req[1]; struct _tds__GetCapabilitiesResponse response[1]; struct soap *soap; int res; req->__sizeCategory = 2; req->Category = (enum tt__CapabilityCategory *)soap_malloc ( soap, 2 * sizeof (enum tt__CapabilityCategory)); *req->Category = tt__CapabilityCategory__Media; *(req->Category + 1) = tt__CapabilityCategory__Events; response->Capabilities = (struct tt__Capabilities *)soap_malloc ( soap, sizeof (struct tt__Capabilities)); xaddr = NULL; res = soap_call___tds__GetCapabilities(soap, uri, req, response); if (res == 0) { if (response->Capabilities != NULL && response->Capabilities->Media) { snprintf (M_XAddr, M_XSize, "%s", response->Capabilities->Media->XAddr); return 0;} } return -1;
}
取得Profiles
取得Capabilites里面的地址之后,就可以根据地址,取得Profile。
取得Profile的函数为soap_call___trt__GetProfiles
。
int
soap_getprofiles (struct gsoap *soap, const char *media_uri)
{ struct _trt__GetProfiles req[1]; struct _trt__GetProfilesResponse response[1]; struct tt__Profile *profile; int res; soap_call___trt__GetProfiles(media_uri, req, response); if (res == 0) { if (response->Profiles != NULL) { for (i = 0; i < response->__sizeProfiles; ++i) { profile = response->Profiles + i; if (profile->VideoSourceConfiguration == NULL || profile->VideoSourceConfiguration->SourceToken == NULL) continue; fprintf (stdout, "Profile token: %s\n", profile->token); if (profile->VideoSourceConfiguration->Bounds) { } if (profile->VideoEncoderConfiguration) { if (op->ve_Encoding == tt__VideoEncoding__H264) { } else if (op->ve_Encoding == tt__VideoEncoding__H265) { } if (profile->VideoEncoderConfiguration->Resolution) { } // profile->VideoEncoderConfiguration->Quality; // profile->VideoEncoderConfiguration->RateControl; } if (profile->AudioEncoderConfiguration) { // profile->AudioEncoderConfiguration->Name); // profile->AudioEncoderConfiguration->Encoding; // profile->AudioEncoderConfiguration->Bitrate; // profile->AudioEncoderConfiguration->SampleRate; } } } } return 0;
}
取得StreamUri
我们遍历了Profile之后,就可以根据我们的意愿,选择取得哪个Profile的媒体地址,使用的函数是soap_call___trt__GetStreamUri
。
const gchar *
soap_getstreamuri (struct gsoap *soap, const char *media_uri, const char *profile, gchar *out, size_t outs
ize)
{ struct _trt__GetStreamUri req[1]; struct _trt__GetStreamUriResponse response[1]; int res; req->StreamSetup = (struct tt__StreamSetup *)soap_malloc ( soap, sizeof (struct tt__StreamSetup)); req->StreamSetup->Stream = 0; req->StreamSetup->Transport = (struct tt__Transport *)soap_malloc ( soap, sizeof (struct tt__Transport)); req->StreamSetup->Transport->Protocol = 0; req->StreamSetup->Transport->Tunnel = 0; req->StreamSetup->__size = 0; req->StreamSetup->__any = NULL; soap_default_xsd__anyAttribute (soap, &req->StreamSetup->__anyAttribute); req->ProfileToken = (char *)profile; res = soap_call___trt__GetStreamUri(soap, media_uri, req, response); if (res == 0) { if (response->MediaUri == NULL) { sh_error ("soap no media uri.\n"); return NULL; } snprintf (out, outsize, "%s", response->MediaUri->Uri); return out; } return NULL;
}
加入Onvif组播
Onvif支持通过组播的方式,向网络里面通告自己的Onvif服务。
绑定UDP的3702端口,加入239.255.255.250地址的组播地址,即可以使用soap_wsdd_listen
来监听Onvif客户端的查询请求。
void
app_listen (struct app_struct *app)
{ struct soap *soap; soap = soap_new1 (SOAP_IO_UDP | SOAP_IO_FLUSH); soap->user = app; soap_set_mode (soap, SOAP_C_UTFSTRING); soap->bind_flags = SO_REUSEADDR; soap_register_plugin (soap, soap_wsa); if (!soap_valid_socket (soap_bind (soap, app->host, 3702, 10))){ fprintf (stderr, "bind %s:%d error: %s\n", app->host, 3702, *soap_faultstring (soap)); soap_print_fault (soap, stderr);} else { struct ip_mreq mcast;mcast.imr_multiaddr.s_addr = inet_addr ("239.255.255.250"); mcast.imr_interface.s_addr = inet_addr (app->host); if (setsockopt (soap->master, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mcast, sizeof (mcast)) < 0) { fprintf (stderr, "onvif set 3702 port group error: %d, %s, %s\n", soap->error, *soap_faultcode (soap), *soap_faultstring (soap));} else { while (soap_wsdd_listen (soap, -100000) == SOAP_OK) { printf ("onvif is listening\n"); }soap_done (soap);}} SOAPCLEANUP (soap); soap_free (soap);
}