Capture vidéo sous MS-windows
Un article de Mangue.org, l'encyclopéde libre.
Les périphériques de captures vidéo sont aujourd'hui devenus classiques. Il suffit de citer les webcams et cartes d'acquisition TV pour s'en convaincre. Il existe des tonnes de logiciels plus ou moins libres pour leurs utilisations, mais qui n'a jamais songé de faire un petit programme pour piloter sa webcam ou sa carte TV pour en faire un magnétoscope ou envoyer la vidéo à un ami ? Nous allons donc vous donner un aperçu des possibilités d'utilisation de vos périphériques en utilisant des fonctions de windows pour se simplifier la vie, le tout dans une interface crée en CBuilder pour sa simplicité de création. La programmation se fera en C++ (mais l'api windows s'interface tout aussi simplement en Pascal Objet pour les utilisateurs de Delphi et en VisualBasic).
| Sommaire |
Gestion des périphériques sous windows
Windows est un systeme d'exploitation intégrant une couche d'abstraction matrielle. A cette couche d'abstraction materielle, vient se greffer des APIs (Application Progamming Interface) permettant d'acceder à certaines fonctionnalitées identiques du système de manière unique. Pour réaliser l'acquisition vidéo, nous allons utiliser l'API VFW (Vidéo For Windows) gérant l'interfaçage des périphériques vidéo (webcam, carte TV, ...).
API Video For Windows
Pour realiser la capture de la video, que ce soit pour le fichier AVI ou pour l'envoie sur le réseau, nous utiliserons la structure CAPTUREPARAMS.
typedef struct { DWORD dwRequestMicroSecPerFrame; BOOL fMakeUserHitOKToCapture; UINT wPercentDropForError; BOOL fYield; DWORD dwIndexSize; UINT wChunkGranularity; BOOL fUsingDOSMemory; UINT wNumVideoRequested; BOOL fCaptureAudio; UINT wNumAudioRequested; UINT vKeyAbort; BOOL fAbortLeftMouse; BOOL fAbortRightMouse; BOOL fLimitEnabled; UINT wTimeLimit; BOOL fMCIControl; BOOL fStepMCIDevice; DWORD dwMCIStartTime; DWORD dwMCIStopTime; BOOL fStepCaptureAt2x; UINT wStepCaptureAverageFrames; DWORD dwAudioBufferSize; BOOL fDisableWriteCache; UINT AVStreamMaster; } CAPTUREPARMS;
Pour realiser notre application, il nous faudra initialiser, parametrer puis activer différentes fonctionalitées de l'API Video For Windows.
// creation d'une fenetre visuelle de capture video HWND VFWAPI capCreateCaptureWindow( LPCSTR lpszWindowName, // Nom de la fenetre (NULL pour nous) DWORD dwStyle, // Caractéristiques de la fenêtre int x, // Position en X dans le conteneur int y, // Position en Y dans le conteneur int nWidth, // Taille en X int nHeight, // Taille en Y HWND hWnd, // Handle du composant graphique conteneur int nID // Identifieur du composant ); BOOL capDriverConnect( hwnd, // handle de la fenetre de capture vidéo iIndex // Numéro du peripherique d'entrée vidéo );
// Affichage de la fenetre de parametrage de l'affichage (si le peripherique le permet) BOOL capDlgVideoDisplay( hwnd // handle de la fenetre de capture video ); // Affichage de la fenetre de parametrage du format d'image (si le peripherique le permet) BOOL capDlgVideoFormat( hwnd // handle de la fenetre de capture video ); // Affichage de la fenetre de reglage de la source video (si le peripherique le permet) BOOL capDlgVideoSource( hwnd // handle de la fenetre de capture video );
// affichage ou non de la video dans la fenetre crée avec capCreateCaptureWindow BOOL capPreview( hwnd, // handle de la fenetre de capture vidéo f // activation (TRUE|FALSE) ); // determination de la periode de rafraichissement de la previsualisation dans la fenetre // ATTENTION ceci n'a rien a voir avec la periode de la capture video !! BOOL capPreviewRate( hwnd, // handle de la fenetre de capture video wMS // periode d'acquisition en ms pour la previsualisation );
// Recuperation des parametres actuels de l'acquisition video BOOL capCaptureGetSetup( hwnd, // handle de la fenetre de capture video s, // adresse de la structure definissant les caracteristiques de la capture (CAPTUREPARAMS) wSize // taille de la structure CAPTUREPARAMS ); // Parametrage de la capture video BOOL capCaptureSetSetup( hwnd, // handle de la fenetre de capture video psCapParms, // adresse de la structure definissant les caracteristiques de la capture (CAPTUREPARAMS) wSize // taille de la structure CAPTUREPARAMS ); // declaration du nom du fichier avi pour sa sauvegarde BOOL capFileSetCaptureFile( hwnd, // handle de la fenetre de capture video szName // pointeur vers chaine de caracteres (NULL terminated) definissant le fichier et son chemin ); // Démarrage de la capture dans le fichier sus-mentionné BOOL capCaptureSequence( hwnd // handle de la fenetre de capture video ); // Démarrage de la capture sans ecriture de fichier avi BOOL capCaptureSequenceNoFile( hwnd // handle de la fenetre de capture video ); // Arrêt de la capture en cours BOOL capCaptureAbort( hwnd // handle de la fenetre de capture video ); // Insertion d'une fonction qui sera appelé a chaque nouvelle image de la capture BOOL capSetCallbackOnVideoStream( hwnd, // handle de la fenetre de capture video fpProc // pointeur vers la fonction [LRESULT CALLBACK capVideoStreamCallback] ); // Fonction permettant de realiser une operation supplementaire sur l'image en cours de la video LRESULT CALLBACK capVideoStreamCallback( HWND hWnd, // handle de l'objet windows appelant (on ne s'en servira pas) LPVIDEOHDR lpVHdr // pointeur vers l'entete de l'image en cours de la video );
Interface CBuilder
Pour realiser l'interface, mon choix c'est porté vers CBuilder car le but n'est pas de passer du temps a creer l'interface mais à faire interagir l'API windows avec notre application. Il est bien sur possible de creer une interface avec VC++ ou Delphi ou VisualBasic... a condition que l'on puisse recuperer le "handle" d'un composant pour afficher la video dedans et que l'on possede le fichier header vfw.h a inclure imperativement pour acceder au fonctions.
L'interface se composera d'une fenetre unique pour se simplifier la compréhension.
- Bouton Source : permettra de faire apparaitre l'interface de configuration de la source video.
- Bouton format : permettra de faire apparaitre l'interface de configuration du format video.
- Bouton affichage : permettra de faire apparaitre la fenetre de configuration du format d'affichage.
- Bouton enregistrer : pour enregistrer la video en cours de previsualisation dans un fichier AVI.
- Bouton réseau : pour effectuer une transmition via reseau de notre video.
- Acquisition du son : ce checkbox permet de specifier si oui ou non on veut enregistrer le son pendant la capture video.
- Socket reseau : ce composant vas nous permettre de realiser rapidement une connexion reseau sans passer par l'imlementation d'un winsock (ce qui est tout a fait possible).
- Composant d'affichage : ce composant est le plus important de notre interface car c'est le handle de ce composant que l'on vas utiliser pour creer notre composant de capture video.
- Selecteur de fichier : ce composant nous permet de faire apparaitre une fenetre de selection de fichier.
- Affichage d'information : cette zone de texte nous permettra d'afficher des informations de debugage.
| Remarque |
| Je rappelle que le choix et l'utilisation de composants CBuilder est tout a fait arbitraire et qu'il est tout a fait possible d'utiliser les fonction de l'API dans tout autre environnement de developpement. |
Initialisation de la capture video
Pour commencer, nous allons creer notre composant de capture video. Ce composant nous sera utile pour tous les appels aux fonctions de gestion de la capture video. Il necessite cependant un composant "support", pour notre cas ce sera le composant d'affichage (un TPanel). Nous ne souhaitons pas utiliser de titre (NULL). Nous voulons que ce soit un composant fils du panel et qu'il soit visible (WS_CHILD|WS_VISIBLE). Nous placerons ce composant au coin en haut à gauche du panel (0,0) pour une taille de 320 par 240 (320,240). Le parent de ce composant sera le panel (rappel : c'est le Handle du composant qu'il nous faut) Panel1->Handle. Et nous lui attribuons un identificateur : 1 (pourquoi pas ?)
// initialisation du panel pour la capture video ... capvideo = capCreateCaptureWindow(NULL,WS_CHILD|WS_VISIBLE,0,0,320,240,Panel1->Handle,1) ;
Nous allons donc maintenant connecter notre fenetre d'affichage au peripherique d'entré, en verifiant que tout s'est bien passé.
// initialisation de la video ... if(!capDriverConnect(capvideo,0 )) { Application->MessageBox("Connexion avec périphérique \nd'entrée vidéo impossible!!","Device Error",MB_OK) ; Form1->Close() ; }
Maintenant que notre periphérique d'entrée est connecté, nous pouvons y acceder. Pour commencer, nous souhaitons avoir une video qui s'affiche sur la totalité du composant et ce quelque soit le format, donc nous appelons capPreviewScale. Puis en fonction des capacités du périphérique, nous activons les boutons d'appels au boite de dialogues correspondantes.
CAPDRIVERCAPS carac ; // Autorisation de l'adaptation du zoom pour etirer l'image a notre format d'affichage capPreviewScale(capvideo,TRUE) ; // recuperation des capacites du peripherique if(capDriverGetCaps(capvideo,&carac,sizeof(carac))) { if(carac.fHasDlgVideoSource) Bouton_source->Enabled = true ; if(carac.fHasDlgVideoFormat) Bouton_format->Enabled = true ; if(carac.fHasDlgVideoDisplay) Bouton_affichage->Enabled = true ; }
Nous sommes maintenant prêt à afficher la vidéo dans notre fenetre.
// reglage de la frequence de rafraichissement (toutes les 33ms) capPreviewRate(capvideo,33) ; // activation de l'affichage continue capPreview(capvideo,1) ;
Parametrage du péripherique
Pour parametrer notre périphérique (webcam, carte TV,...) nous n'allons pas reinventer la roue ! Windows nous met à disposition des interfaces toutes prêtes à l'emploie pour réaliser ces opération fastidieuses à faire à la main. Voici donc les 3 fenêtres de dialogue windows qui vas nous permettre de régler nos paramètres du périphérique.
void __fastcall TForm1::Bouton_sourceClick(TObject *Sender) { capDlgVideoSource(capvideo) ; }
void __fastcall TForm1::Bouton_formatClick(TObject *Sender) { capDlgVideoFormat(capvideo) ; }
void __fastcall TForm1::Bouton_affichageClick(TObject *Sender) { capDlgVideoDisplay(capvideo) ; }
Capture d'un fichier AVI
Il faut tout d'abord donner un nom de fichier. Sous CBuilder cette operation se fait en appelant la methode Execute de notre composant SaveDialog. Si cette methode renvoie TRUE, c'est qu'un fichier a été choisi, et son nom peut etre recupéré par la propriété FileName. Mais cette propriété est une AnsiString et non un "char *" !! Mais pas de problème, les AnsiString ont une méthode c_str qui nous renvoie la chaîne de caractère dans le format qui nous interesse. Il ne nous reste plus qu' a specifier à l'API le nom du fichier avec la fonction capFileSetCaptureFile.
Il nous faut maintenant parametrer la capture. Nous allons donc recuperer les propriété de capture pour modifier ce qui nous interesse. Puis mettre nos réglages en place. Pour cela nous utiliserons les fonctions de l'API capCaptureSetSetup et capCaptureGetSetup.
Enfin nous pouvons initialiser le début de la capture avec capCaptureSequence.
Pour terminer , hé oui il faut bien un jours s'arreter si on ne veut pas déborder du disque, arrêter la capture. Cela sera simple car nous avons paramétré la capture pour bloquer notre interface jusqu'à ce qu'on interrompe la capture par une pression sur la touche ESCAPE.
CAPTUREPARMS capparam ; // lancement de la fenetre de choix de fichier if(SDavi->Execute()) { // si nous sommes ici, c'est qu'un fichier a été selectionné // nous en informons donc l'API capFileSetCaptureFile(capvideo,SDavi->FileName.c_str()) ; // on recupere le parametrage capCaptureGetSetup(capvideo,&capparam,sizeof(capparam)) ; // on regle la periode (en micro-seconde) d'acquisition , ici 15 images/seconde capparam.dwRequestMicroSecPerFrame = 66667 ; // on veut que notre application se fige et attende ESCAPE capparam.fYield = FALSE ; // presentation de la fenetre de depart de capture capparam.fMakeUserHitOKToCapture = TRUE ; // on ne veut pas gerer la fin de capture avec la souris capparam.fAbortLeftMouse = FALSE; capparam.fAbortRightMouse = FALSE; // on annule la capture si on depasse 90% d'images ratés capparam.wPercentDropForError = 90 ; // on capture l'audio au passage si le checkbox son est actif capparam.fCaptureAudio = ((CBson->Checked)?TRUE:FALSE) ; // et pour finir on precise que le flux AVI soit synchronisé dans le temps par le son capparam.AVStreamMaster = AVSTREAMMASTER_AUDIO ; // et HOP! on parametre capCaptureSetSetup(capvideo,&capparam,sizeof(capparam)) ; // et on lance la capture dans la foulée capCaptureSequence(capvideo) ; }
Transmission reseau
Pour enregistrer la video dans un fichier AVI, pas de problème. Comme nous avons pu nous en apercevoir dans la description de la procédure d'enregistrement, c'est l'API VFW qui s'en charge. Mais bon, jusque là, nous n'avons pas vraiment vu les données video passer !! On pourrai se dire qu'il suffit tout simplement de recuperer l'affichage de la previsualisation dans le composant de capture, mais croyez moi, il-y-a nettement moins compliqué ! Pour cela, nous allons utiliser 2 possibilités interressante de la capture video.
Pour commencer, il est possible d'inserer du traitement sur les images du flux vidéo. Ceci s'effectue en mettant en place une fonction "CallBack". Cette fonction sera appelé par le systeme chaque fois qu'une image est disponible. Ce qui est le plus interressant dans cette fonction callback, est qu'elle nous donne en parametre un pointeur vers une structure decrivant l'image actuelle. Cette structure possede en outre 2 données interressante. La premiere est "lpData" qui pointe vers les données de l'image et la seconde est "dwBytesUsed" qui est la taille des données de l'image. Donc grace a cette fonction callback, nous allons pouvoir pour chaque image realiser une operation sur l'image, y compris l'envoyer sur le reseau, ce qui nous interresse. Pour mettre en place cette fonction callback, nous utiliserons la fonction capSetCallbackOnVideoStream avec comme parametre notre fonction de traitement , sans oublier de la retirer lorsque la capture sera terminé en appelant de nouveau capSetCallbackOnVideoStream mais en mettant cette fois-ci NULL comme fonction de traitement.
Il y a cependant un detail qu'il ne faut pas oublier. Le paramètrage de la capture video pour l'enregistrement de l'AVI mettait l'option fYield à FALSE, ce qui avait pour conséquence de figer notre application durant la capture. Ceci est tres genant pour nous car si l'application est gelée, elle ne pourra pas prendre en charge l'envoie, via la connexion réseau, des données de l'image. Il nous faut donc detacher la capture de notre interface en mettant l'option fYield a TRUE. Mais il faudra aussi implemeter l'arret de la capture avec la fonction capCaptureAbort lors d'un click sur un bouton (personnellement j'ais choisi le meme que le debut de l'envoie reseau).
D'accord , vous allez me dire que c'est ok pour envoyer la video sur le reseau, mais le fichier ecrit sur mon disque va vite se retrouver imposant et vas finir par terminer la capture ! Je suis d'accord avec vous, et c'est pour cela que nous ne debuterons pas la capture avec "capCaptureSequence(capvideo)" mais avec "capCaptureSequenceNoFile(capvideo)" qui permet de demarrer la capture video sans ecrire de fichier. Cette fonction n'est utile que si vous avez defini une fonction callback pour traiter les images, sinon quel interêt de capturer la video pour la perdre...
#define SEGMENT 4 LRESULT CALLBACK VideoStreamCallback(HWND hWnd, LPVIDEOHDR lpVHdr) { // nous decoupons l'image a envoyer car celle-ci // correspond a une quantite non negligeable de donnees // qui sera decoupé par la couche IP et qu'il faudra 'recoller' // dans l'application cliente. int y ; for(y=0;y<(120/SEGMENT);y++) { Form1->CSvideo->Socket->SendBuf(lpVHdr->lpData+(y*160*3*SEGMENT),160*3*SEGMENT) ; // on laisse le temps a l'application cliente de recuperer cette partie (1 milliseconde) Sleep(1) ; } return TRUE ; } void __fastcall TForm1::BreseauClick(TObject *Sender) { if(CSvideo->Active) { // on est deja en pleine emission reseau , donc on vas l'arreter capCaptureStop(capvideo) ; // on enleve la fonction callback capSetCallbackOnVideoStream(capvideo, NULL); // on ferme le socket CSvideo->Close() ; // on met un petit message d'information ... Memo1->Lines->Add("IP Closed") ; } else { // on est pas en pleine emission ... donc on commence // on initialise le socket avec l'adresse et le port d'ecoute du client CSvideo->Address = "128.1.75.1" ; CSvideo->Port = 6363 ; // puis on active la connexion .... CSvideo->Open() ; Memo1->Lines->Add("IP Opened") ; } } //--------------------------------------------------------------------------- void __fastcall TForm1::CSvideoConnect(TObject *Sender, TCustomWinSocket *Socket) { // OK on s'est connecte au client // on reconfigure la capture CAPTUREPARMS capparam ; capCaptureGetSetup(capvideo,&capparam,sizeof(capparam)) ; capparam.dwRequestMicroSecPerFrame = 100000 ; // tres important, on detache la capture de l'application .. capparam.fYield = TRUE ; capparam.fMakeUserHitOKToCapture = TRUE ; capparam.fAbortLeftMouse = FALSE; capparam.fAbortRightMouse = FALSE; capparam.wPercentDropForError = 90 ; capparam.fCaptureAudio = FALSE ; capparam.AVStreamMaster = AVSTREAMMASTER_NONE ; capCaptureSetSetup(capvideo,&capparam,sizeof(capparam)) ; // on insere notre fonction d'emission reseau dans le traitement de l'image capSetCallbackOnVideoStream(capvideo, VideoStreamCallback); // on demarre la capture sans fichier capCaptureSequenceNoFile(capvideo) ; } //--------------------------------------------------------------------------- void __fastcall TForm1::CSvideoError(TObject *Sender, TCustomWinSocket *Socket, TErrorEvent ErrorEvent, int &ErrorCode) { // ARG echec de connexion ... on annule tout ! Application->MessageBox("Echec de connexion !!","Network",MB_OK) ; ErrorCode = 0 ; } //--------------------------------------------------------------------------- void __fastcall TForm1::CSvideoDisconnect(TObject *Sender, TCustomWinSocket *Socket) { // on s'est fait deconnecte ... donc on arrete la capture capCaptureStop(capvideo) ; // on enleve la fonction callback capSetCallbackOnVideoStream(capvideo, NULL); } //---------------------------------------------------------------------------
Donc voici la procedure choisi pour envoyer la video sur le reseau. Pour commencer on tente de connecter le Socket à un client en écoute, une fois que cela est fait (methode onConnect sous CBuilder) on met en place la capture. Notre fonction callback emettra l'image en brut dans le socket jusqu'à ce que capCaptureAbort soit appelé, auquel cas nous fermerons le socket et detruirons la fonction callback pour terminer.
Conclusion
Pour illustrer notre interface à l'API Video For Windows, vous pouvez télécharger le programme qui a servi d'exemple (sources + binaires win32). Vous trouverez aussi un exemple de client pour faire fonctionner la partie réseau de notre application.
- CaptureVideo_SourcesCBuilder4.zip (http://mangue.org/cours/imagerie/downloads/CaptureVideo_SourcesCBuilder4.zip)
- ClientVideo_SourcesCBuilder4.zip (http://mangue.org/cours/imagerie/downloads/ClientVideo_SourcesCBuilder4.zip)
Ce tutorial n'a que pour but de vous montrer comment s'interconnecter avec l'API VideoForWindows. Il vas de soit que l'application peut etre nettement perfectible. Pour commencer, pour faire de la capture en AVI, nous devrions pre-allouer le fichier AVI, ceci aurais pour conséquence d'accelerer l'ecriture du fichier sur le disque. Pour ce qui concerne l'emission reseau, il est aussi necessaire de compresser celles-ci ! Mais maintenant que vous savez ecrire un fichier avi et acceder aux images d'un flux video...


