Développement Client ONVIF en C : Contrôle PTZ pour Caméras IP

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.

  1. 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"
  1. 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.

  1. 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;
}

  1. 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

Étiquettes: ONVIF PTZ gSoap C IPC

Publié le 21 juillet à 13h02