Cet article présente une implémentation simplifiée d'un client ONVIF en langage C pour contrôler des caméras IP, en se concentrant sur les fonctionnalités PTZ (Pan-Tilt-Zoom). Le code est conçu pour des caméras dont l'adresse est déjà connue, sans utiliser la découverte automatique.
- Génération des Fichiers d'En-tête
1.1. Préparation de l'environnement
Créez une structure de répertoires :
mkdir -p onvif_project/bin onvif_project/gsoap
Placez les exécutables wsdl2h et soapcpp2 dans le dossier bin/.
Copiez le dossier import et le fichier typemap.dat depuis les sources de gSoap vers gsoap/.
1.2. Création du script de génération
Créez un script generate_headers.sh dans le dossier onvif_project :
#!/bin/bash
mkdir -p onvif_head
cd onvif_head
../bin/wsdl2h -c -o onvif.h -s -d -x -t ../gsoap/typemap.dat \
http://www.onvif.org/onvif/ver10/network/wsdl/remotediscovery.wsdl \
https://www.onvif.org/onvif/ver10/device/wsdl/devicemgmt.wsdl \
http://www.onvif.org/onvif/ver10/media/wsdl/media.wsdl \
http://www.onvif.org/onvif/ver20/ptz/wsdl/ptz.wsdl \
http://www.onvif.org/onvif/ver10/device/wsdl/devicemgmt.wsdl
Rendez le script exécutable et lancez-le :
chmod +x generate_headers.sh
./generate_headers.sh
1.3. Modification du fichier d'en-tête
Ouvrez le fichier onvif.h généré et ajoutez la ligne suivante :
#import "wsse.h"
- Génération du Code Client
Créez le script generate_code.sh :
#!/bin/bash
mkdir -p soap
cd soap
../bin/soapcpp2 -2 -x -c -C ../onvif_head/onvif.h -L -I ../gsoap/import -I ../gsoap/
Exécutez le script. Si vous rencontrez l'erreur suivante :
wsa5.h(290): **ERROR**: service operation name clash: struct/class 'SOAP_ENV__Fault' already declared at wsa.h:278
Ouvrez le fichier gsoap/import/wsa5.h et commentez la déclaration de la structure SOAP_ENV__Fault aux alentours de la ligne 277.
- Implémentation du Programme Principal
Créez un dossier application et copiez-y les fichiers suivants :
- Depuis
soap/:soapC.c,soapClient.c,soapH.h,soapStub.h,wsdd.nsmap - Depuis les sources gSoap :
stdsoap2.c,stdsoap2.h - Depuis
gsoap/import/:dom.c,dom.h - Depuis
gsoap/plugin/:wsseapi.h,wsseapi.c,smdevp.h,smdevp.c,mecevp.c,mecevp.h,threads.c,threads.h,wsaapi.c,wsaapi.h - Depuis
gsoap/custom/:struct_timeval.c,struct_timeval.h
Créez le fichier main.c :
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "soapH.h"
#include "wsdd.nsmap"
#include "soapStub.h"
#include "wsseapi.h"
#include "wsaapi.h"
#include "dom.h"
// Définitions des commandes PTZ
enum PtzDirection {
PTZ_LEFT,
PTZ_RIGHT,
PTZ_UP,
PTZ_DOWN,
PTZ_LEFT_UP,
PTZ_LEFT_DOWN,
PTZ_RIGHT_UP,
PTZ_RIGHT_DOWN,
PTZ_ZOOM_IN,
PTZ_ZOOM_OUT,
};
#define CAM_USER "admin"
#define CAM_PASS "admin"
#define CAM_IP "192.168.1.100"
#define CAM_PORT 8000
#define SERVICE_URL "http://%s:%d/onvif/device_service"
#define TIMEOUT_VAL 10
// Fonctions utilitaires
static void free_string(char **str) {
if (str && *str) {
free(*str);
*str = NULL;
}
}
static struct soap* create_soap_context(int timeout) {
struct soap *ctx = soap_new();
if (!ctx) return NULL;
soap_set_namespaces(ctx, namespaces);
ctx->recv_timeout = timeout;
ctx->send_timeout = timeout;
ctx->connect_timeout = timeout;
#ifdef __linux__
ctx->socket_flags = MSG_NOSIGNAL;
#endif
soap_set_mode(ctx, SOAP_C_UTFSTRING);
return ctx;
}
static void destroy_soap_context(struct soap *ctx) {
if (ctx) {
soap_destroy(ctx);
soap_end(ctx);
soap_done(ctx);
soap_free(ctx);
}
}
static int set_auth(struct soap *ctx) {
return soap_wsse_add_UsernameTokenDigest(ctx, NULL, CAM_USER, CAM_PASS);
}
// Récupération des capacités PTZ
static int fetch_ptz_address(const char *device_url, char **ptz_url) {
struct soap *ctx = NULL;
struct _tds__GetCapabilities req;
struct _tds__GetCapabilitiesResponse resp;
int ret = -1;
memset(&req, 0, sizeof(req));
memset(&resp, 0, sizeof(resp));
ctx = create_soap_context(TIMEOUT_VAL);
if (!ctx) return -1;
ret = soap_call___tds__GetCapabilities(ctx, device_url, NULL, &req, &resp);
if (ret == SOAP_OK && resp.Capabilities && resp.Capabilities->PTZ && resp.Capabilities->PTZ->XAddr) {
*ptz_url = strdup(resp.Capabilities->PTZ->XAddr);
if (!*ptz_url) ret = -1;
} else {
*ptz_url = NULL;
ret = -1;
}
destroy_soap_context(ctx);
return ret;
}
// Obtention du token de profil
static int get_profile_token(const char *ptz_url, char **token) {
struct soap *ctx = NULL;
struct _trt__GetProfiles req;
struct _trt__GetProfilesResponse resp;
int ret = -1;
memset(&req, 0, sizeof(req));
memset(&resp, 0, sizeof(resp));
ctx = create_soap_context(TIMEOUT_VAL);
if (!ctx) return -1;
set_auth(ctx);
ret = soap_call___trt__GetProfiles(ctx, ptz_url, NULL, &req, &resp);
if (ret == SOAP_OK && resp.__sizeProfiles > 0 && resp.Profiles) {
*token = strdup(resp.Profiles[0].token);
if (!*token) ret = -1;
}
destroy_soap_context(ctx);
return ret;
}
// Mouvement absolu
static int ptz_absolute_move(const char *ptz_url, const char *token, float pan, float tilt, float zoom) {
struct soap *ctx = NULL;
struct _tptz__AbsoluteMove req;
struct _tptz__AbsoluteMoveResponse resp;
int ret = -1;
if (!ptz_url || !token) return -1;
ctx = create_soap_context(TIMEOUT_VAL);
if (!ctx) return -1;
set_auth(ctx);
memset(&req, 0, sizeof(req));
req.ProfileToken = soap_strdup(ctx, token);
req.Position = soap_new_tt__PTZVector(ctx, -1);
req.Position->PanTilt = soap_new_tt__Vector2D(ctx, -1);
req.Position->Zoom = soap_new_tt__Vector1D(ctx, -1);
req.Position->PanTilt->x = pan;
req.Position->PanTilt->y = tilt;
req.Position->Zoom->x = zoom;
req.Speed = soap_new_tt__PTZSpeed(ctx, -1);
req.Speed->PanTilt = soap_new_tt__Vector2D(ctx, -1);
req.Speed->Zoom = soap_new_tt__Vector1D(ctx, -1);
req.Speed->PanTilt->x = 0.5f;
req.Speed->PanTilt->y = 0.5f;
req.Speed->Zoom->x = 0.5f;
ret = soap_call___tptz__AbsoluteMove(ctx, ptz_url, NULL, &req, &resp);
if (ret == SOAP_OK) {
printf("Mouvement absolu réussi\n");
}
destroy_soap_context(ctx);
return ret;
}
// Mouvement continu
static int ptz_continuous_move(const char *ptz_url, const char *token, enum PtzDirection dir, float speed) {
struct soap *ctx = NULL;
struct _tptz__ContinuousMove req;
struct _tptz__ContinuousMoveResponse resp;
int ret = -1;
if (!ptz_url || !token) return -1;
ctx = create_soap_context(TIMEOUT_VAL);
if (!ctx) return -1;
set_auth(ctx);
memset(&req, 0, sizeof(req));
req.ProfileToken = (char*)token;
req.Velocity = soap_new_tt__PTZSpeed(ctx, -1);
req.Velocity->PanTilt = soap_new_tt__Vector2D(ctx, -1);
req.Velocity->Zoom = soap_new_tt__Vector1D(ctx, -1);
req.Velocity->PanTilt->space = "http://www.onvif.org/ver10/tptz/PanTiltSpaces/VelocityGenericSpace";
req.Velocity->Zoom->space = "http://www.onvif.org/ver10/tptz/ZoomSpaces/VelocityGenericSpace";
switch (dir) {
case PTZ_LEFT:
req.Velocity->PanTilt->x = -speed; req.Velocity->PanTilt->y = 0; break;
case PTZ_RIGHT:
req.Velocity->PanTilt->x = speed; req.Velocity->PanTilt->y = 0; break;
case PTZ_UP:
req.Velocity->PanTilt->x = 0; req.Velocity->PanTilt->y = speed; break;
case PTZ_DOWN:
req.Velocity->PanTilt->x = 0; req.Velocity->PanTilt->y = -speed; break;
case PTZ_LEFT_UP:
req.Velocity->PanTilt->x = -speed; req.Velocity->PanTilt->y = speed; break;
case PTZ_LEFT_DOWN:
req.Velocity->PanTilt->x = -speed; req.Velocity->PanTilt->y = -speed; break;
case PTZ_RIGHT_UP:
req.Velocity->PanTilt->x = speed; req.Velocity->PanTilt->y = speed; break;
case PTZ_RIGHT_DOWN:
req.Velocity->PanTilt->x = speed; req.Velocity->PanTilt->y = -speed; break;
case PTZ_ZOOM_IN:
req.Velocity->PanTilt->x = 0; req.Velocity->PanTilt->y = 0; req.Velocity->Zoom->x = speed; break;
case PTZ_ZOOM_OUT:
req.Velocity->PanTilt->x = 0; req.Velocity->PanTilt->y = 0; req.Velocity->Zoom->x = -speed; break;
}
ret = soap_call___tptz__ContinuousMove(ctx, ptz_url, NULL, &req, &resp);
destroy_soap_context(ctx);
return ret;
}
// Arrêt du mouvement
static int ptz_stop_move(const char *ptz_url, const char *token) {
struct soap *ctx = NULL;
struct _tptz__Stop req;
struct _tptz__StopResponse resp;
int ret = -1;
if (!ptz_url || !token) return -1;
ctx = create_soap_context(TIMEOUT_VAL);
if (!ctx) return -1;
set_auth(ctx);
memset(&req, 0, sizeof(req));
req.ProfileToken = (char*)token;
ret = soap_call___tptz__Stop(ctx, ptz_url, NULL, &req, &resp);
destroy_soap_context(ctx);
return ret;
}
int main(void) {
char device_url[256];
char *ptz_addr = NULL;
char *token = NULL;
snprintf(device_url, sizeof(device_url), SERVICE_URL, CAM_IP, CAM_PORT);
if (fetch_ptz_address(device_url, &ptz_addr) != 0 || !ptz_addr) {
printf("Impossible d'obtenir l'adresse PTZ\n");
return -1;
}
if (get_profile_token(ptz_addr, &token) != 0 || !token) {
printf("Impossible d'obtenir le token de profil\n");
free_string(&ptz_addr);
return -1;
}
printf("Service PTZ : %s\n", ptz_addr);
printf("Token profil : %s\n", token);
// Exemple : mouvement absolu vers (0, 0, 0)
ptz_absolute_move(ptz_addr, token, 0.0f, 0.0f, 0.0f);
// Exemple commenté : mouvement continu vers la droite
// ptz_continuous_move(ptz_addr, token, PTZ_RIGHT, 0.5f);
// sleep(3);
// ptz_stop_move(ptz_addr, token);
free_string(&ptz_addr);
free_string(&token);
return 0;
}
- Compilation avec Makefile
Créez le fichier Makefile :
CC = gcc
CFLAGS = -DWITH_NONAMESPACES -DWITH_DOM -DWITH_OPENSSL -DWITH_PTHREAD -I. -g -Wall
LDFLAGS = -lssl -lcrypto -lpthread
SRCS = dom.c main.c mecevp.c smdevp.c soapC.c soapClient.c stdsoap2.c struct_timeval.c threads.c wsaapi.c wsseapi.c
OBJS = $(SRCS:.c=.o)
TARGET = onvif_ptz_control
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f $(TARGET) $(OBJS)
.PHONY: all clean
Si vous rencontrez une erreur de double déclaration dans dom.h conecrnant SOAP_ENV__Fault, ouvrez ce fichier et commentez la ligne correspondante. Une fois corrigé, compilez avec :
make