Architecture de base d'un décodeur GPU CUDA
Pour implémenter le décodage de flux vidéo, il est essentiel de comprendre le flux de décodage sous-jacent de CUDA, car l'implémentation matérielle est la même, seuls les appels de niveau supérieur diffèrent. La documentation du SDK vidéo NVIDIA (Video_Codec_SDK) fournit des exemples comme NvTranscoder qui illustrent ce processus.
Le programme principal initialise l'environnement CUDA, crée un décodeur et une file d'attente pour les trames décodées. Un fil de décodage est lancé pour traiter les données d'entrée. Le fil principal extrait ensuite les trames décodées de la file d'attente, les mappe en mémoire GPU, et peut effectuer des traitements suplémentaires avant de les libérer.
// Initialisation simplifiée de l'environnement et des objets CUDA
CUcontext cudaCtx;
CUdevice device;
cuDeviceGet(&device, deviceID);
cuCtxCreate(&cudaCtx, CU_CTX_SCHED_AUTO, device);
CUvideoctxlock ctxLock;
cuCtxPopCurrent(&curCtx);
cuvidCtxLockCreate(&ctxLock, curCtx);
CudaDecoder* decodeur_cuda = new CudaDecoder();
FrameQueue* file_trames = new CUVIDFrameQueue(ctxLock);
decodeur_cuda->Initialize(inputPath, ctxLock, file_trames, width, height);
file_trames->init(width, height);
// Démarrage du fil de décodage (exemple POSIX)
pthread_t thread_decodage;
pthread_create(&thread_decodage, NULL, FonctionDecodage, (void*)decodeur_cuda);
// Boucle principale de traitement des trames
while (!(file_trames->isEndOfDecode() && file_trames->isEmpty())) {
CUVIDPARSERDISPINFO info_trame;
if (file_trames->dequeue(&info_trame)) {
CUdeviceptr ptr_gpu = 0;
unsigned int pas;
CUVIDPROCPARAMS parametres_vp = { 0 };
parametres_vp.progressive_frame = info_trame.progressive_frame;
parametres_vp.top_field_first = info_trame.top_field_first;
cuvidMapVideoFrame(decodeur_cuda->GetDecoder(), info_trame.picture_index, &ptr_gpu, &pas, ¶metres_vp);
// ... traitement des données sur le GPU (ex: copie vers CPU) ...
unsigned int taille_nv12 = pas * (height + height / 2);
cuMemcpyDtoH(buffer_cpu, ptr_gpu, taille_nv12);
cuvidUnmapVideoFrame(decodeur_cuda->GetDecoder(), ptr_gpu);
file_trames->releaseFrame(&info_trame);
}
}
pthread_join(thread_decodage, NULL);
// Nettoyage des ressources...
Construction de l'objet décodeur CUDA
La classe CudaDecoder encapsule trois composants clés: une source vidéo (CUVIDSOURCE), un décodeur (CUVIDDECODER) et un analyseur (CUVIDPARSER). L'initialisation crée d'abord la source vidéo pour recevoir les données brutes. Ensuite, les paramètres vidéo sont extraits pour configurer le décodeur avec les propriétés telles que le codec, la résolution et le format de sortie (NV12). Enfin, l'analyseur est créé pour découper le flux en trames et gérer les callbacks de séquence, décodage et affichage.
// Schéma d'initialisation du décodeur
void CudaDecoder::Initialize(const char* chemin, CUvideoctxlock verrou, FrameQueue* file, int w, int h) {
// 1. Création de la source vidéo
CUVIDSOURCEPARAMS param_source = {0};
param_source.pUserData = this;
param_source.pfnVideoDataHandler = GestionnaireDonneesVideo;
cuvidCreateVideoSource(&m_source, chemin, ¶m_source);
// 2. Création du décodeur
CUVIDEOFORMAT format;
cuvidGetSourceVideoFormat(m_source, &format, 0);
CUVIDDECODECREATEINFO info_creation = {0};
info_creation.CodecType = format.codec;
info_creation.ulWidth = format.coded_width;
info_creation.ulHeight = format.coded_height;
info_creation.OutputFormat = cudaVideoSurfaceFormat_NV12;
// ... autres configurations (surfaces, chroma, etc.) ...
cuvidCreateDecoder(&m_decodeur, &info_creation);
// 3. Création de l'analyseur
CUVIDPARSERPARAMS param_analyseur = {0};
param_analyseur.CodecType = info_creation.CodecType;
param_analyseur.ulMaxNumDecodeSurfaces = info_creation.ulNumDecodeSurfaces;
param_analyseur.pUserData = this;
param_analyseur.pfnSequenceCallback = RetourSequence;
param_analyseur.pfnDecodePicture = RetourDecodageImage;
param_analyseur.pfnDisplayPicture = RetourAffichageImage;
cuvidCreateVideoParser(&m_analyseur, ¶m_analyseur);
}
Le pipeline de données suit cette chaîne: la source fournit des paquets (CUVIDSOURCEDATAPACKET) à l'analyseur via le callback GestionnaireDonneesVideo. L'analyseur traite ces paquets et déclenche RetourDecodageImage pour envoyer les paramètres au décodeur matériel. Une fois décodé, l'image est placée dans la file d'attente via RetourAffichageImage pour consommation par le fil principal.
Intégration avec FFmpeg
Pour utiliser le décodage GPU avec FFmpeg, le processus standard de lecture de paquets via av_read_frame est conservé, mais la conversion de décodage est déléguée à CUDA. Les paquets AVPacket de FFmpeg doivent être convertis en CUVIDSOURCEDATAPACKET et envoyés à l'analyseur CUDA. Une étape cruciale est la gestion du bitstream, notamment pour le H.264 où le format doit être converti en Annex B à l'aide d'un filtre bitstream (BSF) avant l'envoi au décodeur CUDA.
// Boucle de conversion et envoi des paquets
AVPacket paquet_ffmpeg;
while (av_read_frame(contexte_format, &paquet_ffmpeg) >= 0) {
if (paquet_ffmpeg.stream_index == index_flux_video) {
// Filtrage du bitstream si nécessaire (ex: H.264 MP4 vers Annex B)
if (filtre_bsf) {
AVPacket paquet_filtre = {0};
av_packet_ref(&paquet_filtre, &paquet_ffmpeg);
av_bsf_send_packet(filtre_bsf, &paquet_filtre);
av_bsf_receive_packet(filtre_bsf, &paquet_ffmpeg);
}
CUVIDSOURCEDATAPACKET paquet_cuda = {0};
paquet_cuda.payload = paquet_ffmpeg.data;
paquet_cuda.payload_size = (unsigned long)paquet_ffmpeg.size;
if (paquet_ffmpeg.pts != AV_NOPTS_VALUE) {
paquet_cuda.flags = CUVID_PKT_TIMESTAMP;
// Conversion du timestamp vers l'unité CUDA (100 ns)
AVRational tb_cuda = {1, 10000000};
paquet_cuda.timestamp = av_rescale_q(paquet_ffmpeg.pts, contexte_codec->pkt_timebase, tb_cuda);
}
// Envoi à l'analyseur CUDA
cuvidParseVideoData(analyseur_cuda, &paquet_cuda);
av_packet_unref(&paquet_ffmpeg);
}
}
paquet_cuda.flags = CUVID_PKT_ENDOFSTREAM;
cuvidParseVideoData(analyseur_cuda, &paquet_cuda);
Cette approche permet d'exploiter l'accélérationn matérielle de NVIDIA pour le décodage tout en conservant les capacités de démuxage et de gestion de flux de FFmpeg. Les trames décodées en NV12 sur le GPU peuvent ensuite être transférées vers le CPU ou traitées directement pour des opérations ultérieures comme le transcodage ou l'affichage.