Cet article décrit la mise en œuvre de la capture vidéo depuis la caméra d'un appareil iOS, suivie de son encodage matériel en H.264 à l'aide des frameworks AVFoundation et VideoToolbox. Il présente une approche pour configurer une session de capture, gérer les flux vidéo et encoder les images en temps réel.
Pour commencer, définissons l'interface de notre contrôleur de vue principal. Ce contrôleur sera responsalbe de la gestion de la capture vidéo et de l'affichage de la prévisualisation.
#import <AVFoundation/AVFoundation.h>
#import <VideoToolbox/VideoToolbox.h>
#import <UIKit/UIKit.h>
@interface ControleurCaptureVideo : UIViewController <AVCaptureVideoDataOutputSampleBufferDelegate>
// Vue pour afficher la prévisualisation de la caméra
@property (strong, nonatomic) IBOutlet UIImageView *vuePrevisualisationCamera;
// Bouton pour démarrer/arrêter la capture
@property (strong, nonatomic) IBOutlet UIButton *boutonDemarrerArreter;
@end
Le code d'implémentation inclura la configuration de la session de capture, l'initialisation de l'encodeur VideoToolbox et le traitement des buffers d'échantillons vidéo.
// ControleurCaptureVideo.m
#import "ControleurCaptureVideo.h"
#import <CoreGraphics/CoreGraphics.h>
#import <CoreVideo/CoreVideo.h>
#import <CoreMedia/CoreMedia.h>
@interface ControleurCaptureVideo ()
// Référence à la session de compression VideoToolbox
@property (assign, nonatomic) VTCompressionSessionRef sessionCompressionH264;
@end
@implementation ControleurCaptureVideo {
AVCaptureSession *_sessionCapture; // Session de capture AVFoundation
AVCaptureVideoPreviewLayer *_couchePrevisualisation; // Couche pour l'affichage en direct
BOOL _estCaptureActive; // Indicateur de l l'état de la capture
AVCaptureConnection *_connexionVideo; // Connexion pour le flux vidéo
AVCaptureConnection *_connexionAudio; // Connexion pour le flux audio (non utilisé dans cet exemple)
// Variables pour l'écriture du fichier H.264 encodé
NSString *_cheminFichierH264;
NSFileHandle *_gestionnaireFichierH264;
}
#pragma mark - Cycle de vie du contrôleur de vue
- (void)viewDidLoad {
[super viewDidLoad];
// Initialisation et configuration de l'interface utilisateur
_estCaptureActive = NO;
self.vuePrevisualisationCamera.frame = CGRectMake(20, 20, 200, 200);
self.boutonDemarrerArreter.frame = CGRectMake(30, 230, 120, 50);
[self.boutonDemarrerArreter setTitle:@"Démarrer Capture" forState:UIControlStateNormal];
[self.boutonDemarrerArreter addTarget:self action:@selector(toggleCaptureAction:) forControlEvents:UIControlEventTouchUpInside];
// Configuration des composants de capture et d'encodage
[self configurerSessionAVCapture];
[self configurerAffichagePrevisualisation];
[self initialiserEncodeurH264];
}
- (void)dealloc {
// Nettoyage de la session de compression lors de la désallocation
if (_sessionCompressionH264) {
VTCompressionSessionCompleteFrames(_sessionCompressionH264, kCMTimeInvalid);
VTCompressionSessionInvalidate(_sessionCompressionH264);
CFRelease(_sessionCompressionH264);
_sessionCompressionH264 = NULL;
}
// Fermer le fichier si ouvert
[_gestionnaireFichierH264 closeFile];
}
#pragma mark - Configuration de la capture vidéo
// Configure la session de capture AVFoundation
- (void)configurerSessionAVCapture {
_sessionCapture = [[AVCaptureSession alloc] init];
_sessionCapture.usesApplicationAudioSession = NO; // Éviter les conflits audio
NSError *erreur = nil;
AVCaptureDevice *appareilVideo = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
AVCaptureDeviceInput *entreeVideo = [AVCaptureDeviceInput deviceInputWithDevice:appareilVideo error:&erreur];
if (erreur) {
NSLog(@"Erreur lors de l'initialisation de l'entrée vidéo : %@", erreur.localizedDescription);
return;
}
// Tenter d'ajouter l'entrée vidéo à la session
if ([_sessionCapture canAddInput:entreeVideo]) {
[_sessionCapture addInput:entreeVideo];
} else {
NSLog(@"Impossible d'ajouter l'entrée vidéo à la session.");
return;
}
// Configuration du débit d'images (framerate)
// AVFoundation gère la résolution et le framerate de manière complexe.
// L'approche ici est de définir une durée minimale/maximale pour chaque image.
NSInteger framerateDesire = 30; // Nombre d'images par seconde souhaité
CMTime minFrameDuration = CMTimeMake(1, (int32_t)framerateDesire);
CMTime maxFrameDuration = minFrameDuration;
// Vérifier les formats supportés pour le périphérique
[appareilVideo lockForConfiguration:nil];
appareilVideo.activeVideoMinFrameDuration = minFrameDuration;
appareilVideo.activeVideoMaxFrameDuration = maxFrameDuration;
[appareilVideo unlockForConfiguration];
// Configuration de la sortie des données vidéo
AVCaptureVideoDataOutput *sortieDonneesVideo = [[AVCaptureVideoDataOutput alloc] init];
sortieDonneesVideo.alwaysDiscardsLateVideoFrames = YES; // Ne pas conserver les images en retard
// Définir le format des pixels à YUV 420 bi-planar (NV12)
NSDictionary *reglagesVideo = @{
(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)
};
[sortieDonneesVideo setVideoSettings:reglagesVideo];
// File d'attente pour le délégué de sortie
dispatch_queue_t fileAttenteVideo = dispatch_queue_create("fr.capture.videoQueue", NULL);
[sortieDonneesVideo setSampleBufferDelegate:self queue:fileAttenteVideo];
// Tenter d'ajouter la sortie vidéo à la session
if ([_sessionCapture canAddOutput:sortieDonneesVideo]) {
[_sessionCapture addOutput:sortieDonneesVideo];
} else {
NSLog(@"Impossible d'ajouter la sortie vidéo à la session.");
return;
}
// Récupérer les connexions pour identifier les données vidéo/audio
_connexionVideo = [sortieDonneesVideo connectionWithMediaType:AVMediaTypeVideo];
_connexionAudio = [sortieDonneesVideo connectionWithMediaType:AVMediaTypeAudio]; // Non utilisé pour l'encodage vidéo
// Définir la résolution de la session
if ([_sessionCapture canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
_sessionCapture.sessionPreset = AVCaptureSessionPreset1280x720;
} else {
NSLog(@"Impossible de définir la résolution AVCaptureSessionPreset1280x720.");
}
}
// Configure la couche de prévisualisation de la caméra
- (void)configurerAffichagePrevisualisation {
_couchePrevisualisation = [AVCaptureVideoPreviewLayer layerWithSession:_sessionCapture];
_couchePrevisualisation.videoGravity = AVLayerVideoGravityResizeAspectFill; // Mode de remplissage
// S'assurer que la prévisualisation est en mode portrait
if ([_couchePrevisualisation.connection isVideoOrientationSupported]) {
[_couchePrevisualisation.connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
}
_couchePrevisualisation.frame = self.vuePrevisualisationCamera.layer.bounds;
[self.vuePrevisualisationCamera.layer addSublayer:_couchePrevisualisation];
}
#pragma mark - Gestion de la capture
- (IBAction)toggleCaptureAction:(id)sender {
if (_estCaptureActive) {
[self arreterCapture];
} else {
[self demarrerCapture];
}
}
// Démarre la capture vidéo
- (BOOL)demarrerCapture {
if (_estCaptureActive) {
return NO;
}
// Vérifier les permissions de la caméra
AVAuthorizationStatus statutVideo = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
if (statutVideo == AVAuthorizationStatusDenied || statutVideo == AVAuthorizationStatusRestricted) {
NSLog(@"Accès à la caméra refusé. Veuillez accorder les permissions dans les réglages.");
return NO;
}
// Nettoyer le fichier H.264 existant et en créer un nouveau pour l'écriture
NSArray *cheminsDoc = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *repertoireDocuments = [cheminsDoc objectAtIndex:0];
_cheminFichierH264 = [repertoireDocuments stringByAppendingPathComponent:@"video_encodee.h264"];
[[NSFileManager defaultManager] removeItemAtPath:_cheminFichierH264 error:nil];
[[NSFileManager defaultManager] createFileAtPath:_cheminFichierH264 contents:nil attributes:nil];
_gestionnaireFichierH264 = [NSFileHandle fileHandleForWritingAtPath:_cheminFichierH264];
if (!_gestionnaireFichierH264) {
NSLog(@"Erreur : Impossible d'ouvrir le fichier de sortie H.264.");
return NO;
}
[_sessionCapture startRunning];
_estCaptureActive = YES;
[self.boutonDemarrerArreter setTitle:@"Arrêter Capture" forState:UIControlStateNormal];
NSLog(@"Capture démarrée et encodage en cours vers : %@", _cheminFichierH264);
return YES;
}
// Arrête la capture vidéo
- (BOOL)arreterCapture {
if (!_estCaptureActive) {
return NO;
}
[_sessionCapture stopRunning];
_estCaptureActive = NO;
[self.boutonDemarrerArreter setTitle:@"Démarrer Capture" forState:UIControlStateNormal];
// Finaliser la session d'encodage et fermer le fichier
[self arreterEncodageVideo];
[_gestionnaireFichierH264 closeFile];
_gestionnaireFichierH264 = nil;
NSLog(@"Capture arrêtée et fichier H.264 fermé.");
return YES;
}
#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate
// Délégué appelé lorsque de nouvelles données vidéo/audio sont disponibles
- (void)captureOutput:(AVCaptureOutput *)output
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
if (connection == _connexionVideo) {
// C'est un buffer vidéo
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
if (!pixelBuffer) {
NSLog(@"Le sampleBuffer vidéo ne contient pas de CVPixelBuffer.");
return;
}
// Verrouiller le buffer de pixels pour accéder aux données
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
// Encodage H.264
[self encoderFrameVideo:sampleBuffer forcerImageClef:NO];
// Déverrouiller le buffer de pixels
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
} else if (connection == _connexionAudio) {
// C'est un buffer audio, ignorer pour cet exemple
}
}
#pragma mark - Encodage H.264 avec VideoToolbox
// Callback appelé par VideoToolbox lorsque l'encodage est terminé pour une frame
void encodageH264Callback(void * CM_NULLABLE outputCallbackRefCon,
void * CM_NULLABLE sourceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CM_NULLABLE CMSampleBufferRef sampleBuffer) {
if (noErr != status || sampleBuffer == NULL) {
NSLog(@"Erreur d'encodage H.264 : %d", (int)status);
return;
}
if (!CMSampleBufferDataIsReady(sampleBuffer)) {
NSLog(@"Le sampleBuffer encodé n'est pas prêt.");
return;
}
// Récupérer l'instance du contrôleur pour accéder au gestionnaire de fichier
ControleurCaptureVideo *selfRef = (__bridge ControleurCaptureVideo *)outputCallbackRefCon;
if (!selfRef || !selfRef->_gestionnaireFichierH264) {
return;
}
// Préfixe NAL unit (start code)
const uint8_t enteteNAL[] = {0x00, 0x00, 0x00, 0x01};
NSData *dataEnteteNAL = [NSData dataWithBytes:enteteNAL length:sizeof(enteteNAL)];
// Vérifier si c'est une image clé (I-frame)
BOOL estImageClef = !CFDictionaryContainsKey((CFDictionaryRef)CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0), kCMSampleAttachmentKey_NotSync);
if (estImageClef) {
NSLog(@"Image clé H.264 encodée.");
// Pour les images clés, extraire les paramètres SPS et PPS
CMFormatDescriptionRef descriptionFormat = CMSampleBufferGetFormatDescription(sampleBuffer);
// SPS (Sequence Parameter Set)
size_t tailleSPS, nombreSPS;
const uint8_t *ptrSPS;
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(descriptionFormat, 0, &ptrSPS, &tailleSPS, &nombreSPS, NULL);
NSData *spsData = [NSData dataWithBytes:ptrSPS length:tailleSPS];
// PPS (Picture Parameter Set)
size_t taillePPS, nombrePPS;
const uint8_t *ptrPPS;
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(descriptionFormat, 1, &ptrPPS, &taillePPS, &nombrePPS, NULL);
NSData *ppsData = [NSData dataWithBytes:ptrPPS length:taillePPS];
// Écrire SPS et PPS au fichier, chacun précédé du start code
NSMutableData *dataSPSComplete = [NSMutableData dataWithData:dataEnteteNAL];
[dataSPSComplete appendData:spsData];
[selfRef->_gestionnaireFichierH264 writeData:dataSPSComplete];
NSMutableData *dataPPSComplete = [NSMutableData dataWithData:dataEnteteNAL];
[dataPPSComplete appendData:ppsData];
[selfRef->_gestionnaireFichierH264 writeData:dataPPSComplete];
}
// Récupérer les données NAL de la frame encodée
CMBlockBufferRef bufferBloc = CMSampleBufferGetDataBuffer(sampleBuffer);
size_t longueurNAL, longueurTotaleNAL;
char *pointeurDonneesNAL;
status = CMBlockBufferGetDataPointer(bufferBloc, 0, &longueurNAL, &longueurTotaleNAL, &pointeurDonneesNAL);
if (noErr != status) {
NSLog(@"Erreur CMBlockBufferGetDataPointer : %d", (int)status);
return;
}
// Les unités NAL dans le CMSampleBuffer sont préfixées par leur longueur (4 octets, big-endian)
size_t offsetBuffer = 0;
const int longueurEnteteAVC = 4; // Longueur du champ de taille NAL
while (offsetBuffer < longueurTotaleNAL - longueurEnteteAVC) {
uint32_t longueurUniteNAL = 0;
memcpy(&longueurUniteNAL, pointeurDonneesNAL + offsetBuffer, longueurEnteteAVC);
longueurUniteNAL = CFSwapInt32BigToHost(longueurUniteNAL); // Convertir big-endian en hôte
if (longueurUniteNAL > longueurTotaleNAL - offsetBuffer - longueurEnteteAVC) {
NSLog(@"Erreur : Longueur NAL invalide, potentielle corruption de données.");
break;
}
NSData *dataUniteNAL = [[NSData alloc] initWithBytes:(pointeurDonneesNAL + offsetBuffer + longueurEnteteAVC)
length:longueurUniteNAL];
// Écrire le start code suivi des données NAL
NSMutableData *dataFrameComplete = [NSMutableData dataWithData:dataEnteteNAL];
[dataFrameComplete appendData:dataUniteNAL];
[selfRef->_gestionnaireFichierH264 writeData:dataFrameComplete];
offsetBuffer += longueurEnteteNAL + longueurUniteNAL;
}
[selfRef->_gestionnaireFichierH264 synchronizeFile]; // S'assurer que les données sont écrites sur le disque
}
// Initialise la session de compression H.264
- (void)initialiserEncodeurH264 {
// Créer la session de compression VideoToolbox
OSStatus status = VTCompressionSessionCreate(NULL,
1280, // Largeur de la vidéo
720, // Hauteur de la vidéo
kCMVideoCodecType_H264, // Type de codec
NULL, // Dictionnaire d'allocateur (par défaut)
NULL, // Dictionnaire d'attributs de source d'image (par défaut)
NULL, // Dictionnaire d'encodeur (par défaut)
encodageH264Callback, // Fonction de callback pour la sortie
(__bridge void *)(self), // Contexte de référence pour le callback (ici, le contrôleur)
&_sessionCompressionH264);
if (noErr != status) {
NSLog(@"Erreur VTCompressionSessionCreate : %d", (int)status);
return;
}
// Définir le débit binaire moyen (Average Bit Rate)
// 512 kbps, exemple. Ajustez selon les besoins de qualité/bande passante.
status = VTSessionSetProperty(_sessionCompressionH264,
kVTCompressionPropertyKey_AverageBitRate,
(__bridge CFTypeRef)@(512 * 1024));
// Définir le profil H.264 (Baseline 3.1 pour une large compatibilité)
status = VTSessionSetProperty(_sessionCompressionH264,
kVTCompressionPropertyKey_ProfileLevel,
kVTProfileLevel_H264_Baseline_3_1);
// Activer l'encodage en temps réel pour minimiser la latence
status = VTSessionSetProperty(_sessionCompressionH264,
kVTCompressionPropertyKey_RealTime,
kCFBooleanTrue);
// Désactiver le réordonnancement des frames (pas de B-frames) pour un stream plus simple
status = VTSessionSetProperty(_sessionCompressionH264,
kVTCompressionPropertyKey_AllowFrameReordering,
kCFBooleanFalse);
// Définir le framerate attendu
status = VTSessionSetProperty(_sessionCompressionH264,
kVTCompressionPropertyKey_ExpectedFrameRate,
(__bridge CFTypeRef)@(30));
// Définir l'intervalle maximum entre les images clés (GOP size)
// Ici, une image clé toutes les 2 secondes (30 fps * 2s = 60 frames)
status = VTSessionSetProperty(_sessionCompressionH264,
kVTCompressionPropertyKey_MaxKeyFrameInterval,
(__bridge CFTypeRef)@(60));
// Définir la durée maximale de l'intervalle entre les images clés
status = VTSessionSetProperty(_sessionCompressionH264,
kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration,
(__bridge CFTypeRef)@(2.0));
// Préparer l'encodeur à recevoir des frames
status = VTCompressionSessionPrepareToEncodeFrames(_sessionCompressionH264);
if (noErr != status) {
NSLog(@"Erreur VTCompressionSessionPrepareToEncodeFrames : %d", (int)status);
}
}
// Soumet une frame vidéo pour l'encodage H.264
- (BOOL)encoderFrameVideo:(CMSampleBufferRef)sampleBuffer forcerImageClef:(BOOL)forcerImageClef {
if (_sessionCompressionH264 == NULL || sampleBuffer == NULL) {
return NO;
}
CVImageBufferRef bufferPixel = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
// Options pour forcer une image clé
NSDictionary *proprietesFrame = nil;
if (forcerImageClef) {
proprietesFrame = @{(__bridge NSString *)kVTEncodeFrameOptionKey_ForceKeyFrame : @(YES)};
}
// Envoyer la frame à la session de compression
OSStatus status = VTCompressionSessionEncodeFrame(_sessionCompressionH264,
bufferPixel,
CMSampleBufferGetPresentationTimeStamp(sampleBuffer), // Horodatage de présentation
kCMTimeInvalid, // Durée non spécifiée, l'encodeur la détermine
(__bridge CFDictionaryRef)proprietesFrame, // Propriétés de la frame
NULL, // Contexte de référence de la frame (non utilisé)
NULL); // Info de la frame (non utilisé)
if (noErr != status) {
NSLog(@"Erreur VTCompressionSessionEncodeFrame : %d", (int)status);
return NO;
}
return YES;
}
// Arrête la session d'encodage vidéo
- (BOOL)arreterEncodageVideo {
if (_sessionCompressionH264 == NULL) {
return NO;
}
// Demander à la session de compression de finaliser toutes les frames en attente
OSStatus status = VTCompressionSessionCompleteFrames(_sessionCompressionH264, kCMTimeInvalid);
if (noErr != status) {
NSLog(@"Erreur VTCompressionSessionCompleteFrames : %d", (int)status);
return NO;
}
return YES;
}
@end