web-dev-qa-db-fra.com

Comment encoder une vidéo à partir de plusieurs images générées dans un programme C ++ sans écrire les images d'images séparées sur le disque?

J'écris un code C++ où une séquence de N trames différentes est générée après avoir effectué certaines opérations implémentées dedans. Une fois chaque image terminée, je l'écris sur le disque en tant que IMG_% d.png, et enfin je les encode en vidéo via ffmpeg en utilisant le codec x264.

Le pseudocode résumé de la partie principale du programme est le suivant:

std::vector<int> B(width*height*3);
for (i=0; i<N; i++)
{
  // void generateframe(std::vector<int> &, int)
  generateframe(B, i); // Returns different images for different i values.
  sprintf(s, "IMG_%d.png", i+1);
  WriteToDisk(B, s); // void WriteToDisk(std::vector<int>, char[])
}

Le problème de cette implémentation est que le nombre d'images souhaitées, N, est généralement élevé (N ~ 100000) ainsi que la résolution des images (1920x1080), entraînant une surcharge du disque, produisant des cycles d'écriture de dizaines de Go après chaque exécution.

Afin d'éviter cela, j'ai essayé de trouver de la documentation sur l'analyse directe de chaque image stockée dans le vecteur B vers un encodeur tel que x264 (sans avoir à écrire les fichiers image intermédiaires sur le disque). Bien que certains sujets intéressants aient été trouvés, aucun n'a résolu spécifiquement ce que je voulais exactement, car beaucoup d'entre eux concernent l'exécution de l'encodeur avec des fichiers d'images existants sur le disque, tandis que d'autres fournissent des solutions pour d'autres langages de programmation tels que Python ( ici vous pouvez trouver une solution entièrement satisfaisante pour cette plate-forme).

Le pseudocode de ce que j'aimerais obtenir est quelque chose de similaire à ceci:

std::vector<int> B(width*height*3);
video_file=open_video("Generated_Video.mp4", ...[encoder options]...);
for (i=0; i<N; i++)
{
  generateframe(B, i+1);
  add_frame(video_file, B);
}
video_file.close();

D'après ce que j'ai lu sur des sujets connexes, l'API x264 C++ pourrait être en mesure de le faire, mais, comme indiqué ci-dessus, je n'ai pas trouvé de réponse satisfaisante à ma question spécifique. J'ai essayé d'apprendre et d'utiliser directement le code source de ffmpeg, mais sa faible facilité d'utilisation et ses problèmes de compilation m'ont forcé à rejeter cette possibilité en tant que simple programmeur non professionnel que je suis (je le prends comme un hobby et malheureusement je ne peux pas le gaspiller autant de fois à apprendre quelque chose de si exigeant).

Une autre solution possible qui m'est venue à l'esprit est de trouver un moyen d'appeler le fichier binaire ffmpeg dans le code C++, et de réussir à transférer en quelque sorte les données d'image de chaque itération (stockées en B) vers l'encodeur, en laissant l'ajout de chaque trame (c'est-à-dire ne pas "fermer" le fichier vidéo à écrire) jusqu'à la dernière image, afin que plus d'images puissent être ajoutées jusqu'à atteindre la N-ème, où le fichier vidéo sera "fermé". En d'autres termes, appelez ffmpeg.exe via le programme C++ pour écrire la première image dans une vidéo, mais faites en sorte que l'encodeur "attende" d'autres images. Ensuite, appelez à nouveau ffmpeg pour ajouter la deuxième image et faire en sorte que l'encodeur "attende" encore plus d'images, et ainsi de suite jusqu'à atteindre la dernière image, où la vidéo sera terminée. Cependant, je ne sais pas comment procéder ni si c'est réellement possible.

Édition 1:

Comme suggéré dans les réponses, j'ai documenté des canaux nommés et essayé de les utiliser dans mon code. Tout d'abord, il convient de noter que je travaille avec Cygwin, donc mes canaux nommés sont créés comme ils le seraient sous Linux. Le pseudocode modifié que j'ai utilisé (y compris les bibliothèques système correspondantes) est le suivant:

FILE *fd;
mkfifo("myfifo", 0666);

for (i=0; i<N; i++)
{
  fd=fopen("myfifo", "wb");
  generateframe(B, i+1);
  WriteToPipe(B, fd); // void WriteToPipe(std::vector<int>, FILE *&fd)
  fflush(fd);
  fd=fclose("myfifo");
}
unlink("myfifo");

WriteToPipe est une légère modification de la fonction WriteToFile précédente, où je me suis assuré que le tampon d'écriture pour envoyer les données d'image est suffisamment petit pour s'adapter aux limitations de mise en mémoire tampon du tuyau.

Ensuite, je compile et écris la commande suivante dans le terminal Cygwin:

./myprogram | ffmpeg -i pipe:myfifo -c:v libx264 -preset slow -crf 20 Video.mp4

Cependant, il reste bloqué sur la boucle lorsque i = 0 sur la ligne "fopen" (c'est-à-dire le premier appel fopen). Si je n'avais pas appelé ffmpeg, ce serait naturel car le serveur (mon programme) attendrait qu'un programme client se connecte à "l'autre côté" du pipe, mais ce n'est pas le cas. Il semble qu'ils ne puissent pas être connectés via le tuyau d'une manière ou d'une autre, mais je n'ai pas pu trouver de documentation supplémentaire afin de résoudre ce problème. Toute suggestion?

19
ksb496

Après une lutte intense, j'ai finalement réussi à le faire fonctionner après avoir appris un peu comment utiliser les API FFmpeg et libx264 C pour mon objectif spécifique, grâce aux informations utiles que certains utilisateurs ont fournies sur ce site et d'autres, ainsi que d'autres Exemples de documentation de FFmpeg. Par souci d'illustration, les détails seront présentés ci-dessous.

Tout d'abord, la bibliothèque C libx264 a été compilée et, après cela, celle FFmpeg avec les options de configuration --enable-gpl --enable-libx264. Passons maintenant au codage. La partie pertinente du code qui a atteint l'objectif recherché est la suivante:

Comprend:

#include <stdint.h>
extern "C"{
#include <x264.h>
#include <libswscale/swscale.h>
#include <libavcodec/avcodec.h>
#include <libavutil/mathematics.h>
#include <libavformat/avformat.h>
#include <libavutil/opt.h>
}

LDFLAGS sur Makefile:

-lx264 -lswscale -lavutil -lavformat -lavcodec

Code interne (par souci de simplicité, les vérifications d'erreurs seront omises et les déclarations de variables seront effectuées en cas de besoin au lieu du début pour une meilleure compréhension):

av_register_all(); // Loads the whole database of available codecs and formats.

struct SwsContext* convertCtx = sws_getContext(width, height, AV_PIX_FMT_RGB24, width, height, AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR, NULL, NULL, NULL); // Preparing to convert my generated RGB images to YUV frames.

// Preparing the data concerning the format and codec in order to write properly the header, frame data and end of file.
char *fmtext="mp4";
char *filename;
sprintf(filename, "GeneratedVideo.%s", fmtext);
AVOutputFormat * fmt = av_guess_format(fmtext, NULL, NULL);
AVFormatContext *oc = NULL;
avformat_alloc_output_context2(&oc, NULL, NULL, filename);
AVStream * stream = avformat_new_stream(oc, 0);
AVCodec *codec=NULL;
AVCodecContext *c= NULL;
int ret;

codec = avcodec_find_encoder_by_name("libx264");

// Setting up the codec:
av_dict_set( &opt, "preset", "slow", 0 );
av_dict_set( &opt, "crf", "20", 0 );
avcodec_get_context_defaults3(stream->codec, codec);
c=avcodec_alloc_context3(codec);
c->width = width;
c->height = height;
c->pix_fmt = AV_PIX_FMT_YUV420P;

// Setting up the format, its stream(s), linking with the codec(s) and write the header:
if (oc->oformat->flags & AVFMT_GLOBALHEADER) // Some formats require a global header.
    c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
avcodec_open2( c, codec, &opt );
av_dict_free(&opt);
stream->time_base=(AVRational){1, 25};
stream->codec=c; // Once the codec is set up, we need to let the container know which codec are the streams using, in this case the only (video) stream.
av_dump_format(oc, 0, filename, 1);
avio_open(&oc->pb, filename, AVIO_FLAG_WRITE);
ret=avformat_write_header(oc, &opt);
av_dict_free(&opt); 

// Preparing the containers of the frame data:
AVFrame *rgbpic, *yuvpic;

// Allocating memory for each RGB frame, which will be lately converted to YUV:
rgbpic=av_frame_alloc();
rgbpic->format=AV_PIX_FMT_RGB24;
rgbpic->width=width;
rgbpic->height=height;
ret=av_frame_get_buffer(rgbpic, 1);

// Allocating memory for each conversion output YUV frame:
yuvpic=av_frame_alloc();
yuvpic->format=AV_PIX_FMT_YUV420P;
yuvpic->width=width;
yuvpic->height=height;
ret=av_frame_get_buffer(yuvpic, 1);

// After the format, code and general frame data is set, we write the video in the frame generation loop:
// std::vector<uint8_t> B(width*height*3);

Le vecteur commenté ci-dessus a la même structure que celui que j'ai exposé dans ma question; cependant, les données RVB sont stockées sur les AVFrames d'une manière spécifique. Par conséquent, pour des raisons d'exposition, supposons que nous ayons plutôt un pointeur vers une structure de la forme uint8_t [3] Matrix (int, int), dont le moyen d'accéder aux valeurs de couleur des pixels pour une coordonnée donnée (x, y) est Matrix (x, y) -> Red, Matrix (x, y) -> Green et Matrix (x, y) -> Blue, afin d'obtenir, respectivement, les valeurs rouge, verte et bleue du coordonnée (x, y). Le premier argument représente la position horizontale, de gauche à droite lorsque x augmente et le second la position verticale, de haut en bas lorsque y augmente.

Cela dit, la boucle for pour transférer les données, encoder et écrire chaque trame serait la suivante:

Matrix B(width, height);
int got_output;
AVPacket pkt;
for (i=0; i<N; i++)
{
    generateframe(B, i); // This one is the function that generates a different frame for each i.
    // The AVFrame data will be stored as RGBRGBRGB... row-wise, from left to right and from top to bottom, hence we have to proceed as follows:
    for (y=0; y<height; y++)
    {
        for (x=0; x<width; x++)
        {
            // rgbpic->linesize[0] is equal to width.
            rgbpic->data[0][y*rgbpic->linesize[0]+3*x]=B(x, y)->Red;
            rgbpic->data[0][y*rgbpic->linesize[0]+3*x+1]=B(x, y)->Green;
            rgbpic->data[0][y*rgbpic->linesize[0]+3*x+2]=B(x, y)->Blue;
        }
    }
    sws_scale(convertCtx, rgbpic->data, rgbpic->linesize, 0, height, yuvpic->data, yuvpic->linesize); // Not actually scaling anything, but just converting the RGB data to YUV and store it in yuvpic.
    av_init_packet(&pkt);
    pkt.data = NULL;
    pkt.size = 0;
    yuvpic->pts = i; // The PTS of the frame are just in a reference unit, unrelated to the format we are using. We set them, for instance, as the corresponding frame number.
    ret=avcodec_encode_video2(c, &pkt, yuvpic, &got_output);
    if (got_output)
    {
        fflush(stdout);
        av_packet_rescale_ts(&pkt, (AVRational){1, 25}, stream->time_base); // We set the packet PTS and DTS taking in the account our FPS (second argument) and the time base that our selected format uses (third argument).
        pkt.stream_index = stream->index;
        printf("Write frame %6d (size=%6d)\n", i, pkt.size);
        av_interleaved_write_frame(oc, &pkt); // Write the encoded frame to the mp4 file.
        av_packet_unref(&pkt);
    }
}
// Writing the delayed frames:
for (got_output = 1; got_output; i++) {
    ret = avcodec_encode_video2(c, &pkt, NULL, &got_output);
    if (got_output) {
        fflush(stdout);
        av_packet_rescale_ts(&pkt, (AVRational){1, 25}, stream->time_base);
        pkt.stream_index = stream->index;
        printf("Write frame %6d (size=%6d)\n", i, pkt.size);
        av_interleaved_write_frame(oc, &pkt);
        av_packet_unref(&pkt);
    }
}
av_write_trailer(oc); // Writing the end of the file.
if (!(fmt->flags & AVFMT_NOFILE))
    avio_closep(oc->pb); // Closing the file.
avcodec_close(stream->codec);
// Freeing all the allocated memory:
sws_freeContext(convertCtx);
av_frame_free(&rgbpic);
av_frame_free(&yuvpic);
avformat_free_context(oc);

Notes annexes:

Pour référence future, comme les informations disponibles sur le net concernant les horodatages (PTS/DTS) semblent si confuses, je vais ensuite expliquer également comment j'ai réussi à résoudre les problèmes en définissant les valeurs appropriées. La définition incorrecte de ces valeurs a fait que la taille de sortie était beaucoup plus grande que celle obtenue via l'outil de ligne de commande binaire construit ffmpeg, car les données de trame étaient écrites de manière redondante sur des intervalles de temps plus petits que ceux réellement définis par le FPS.

Tout d'abord, il convient de noter que lors du codage, il existe deux types d'horodatages: un associé à la trame (PTS) (étape de pré-codage) et deux associés au paquet (PTS et DTS) (étape de post-codage) . Dans le premier cas, il semble que les valeurs PTS de trame puissent être attribuées à l'aide d'une unité de référence personnalisée (avec la seule restriction qu'elles doivent être également espacées si l'on veut un FPS constant), donc on peut prendre par exemple le numéro de trame comme nous a fait dans le code ci-dessus. Dans le second, nous devons prendre en compte les paramètres suivants:

  • La base de temps du conteneur de format de sortie, dans notre cas mp4 (= 12800 Hz), dont les informations sont conservées dans stream-> time_base.
  • Le FPS souhaité de la vidéo.
  • Si l'encodeur génère des images B ou non (dans le second cas, les valeurs PTS et DTS pour la trame doivent être définies de la même manière, mais c'est plus compliqué si nous sommes dans le premier cas, comme dans cet exemple.) Voir ceci réponse à une autre question connexe pour plus de références.

La clé ici est que, heureusement, il n'est pas nécessaire de lutter avec le calcul de ces quantités, car libav fournit une fonction pour calculer les horodatages corrects associés au paquet en connaissant les données susmentionnées:

av_packet_rescale_ts(AVPacket *pkt, AVRational FPS, AVRational time_base)

Grâce à ces considérations, j'ai finalement pu générer un conteneur de sortie sain et essentiellement le même taux de compression que celui obtenu à l'aide de l'outil de ligne de commande, qui étaient les deux problèmes restants avant d'étudier plus en détail comment l'en-tête et la bande-annonce du format et comment l'heure les tampons sont correctement placés.

21
ksb496

Merci pour votre excellent travail, @ ksb496!

Une amélioration mineure:

c=avcodec_alloc_context3(codec);

devrait être mieux écrit comme:

c = stream->codec;

pour éviter une fuite de mémoire.

Si cela ne vous dérange pas, j'ai téléchargé la bibliothèque complète prête à déployer sur GitHub: https://github.com/apc-llc/moviemaker-cpp.git

1
Dmitry Mikushin

API: avcodec_encode_video2 & avcodec_encode_audio2 semble obsolète. FFmpeg de la version actuelle (4.2) a une nouvelle API: avcodec_send_frame & avcodec_receive_packet.

0
Andy Chou

Grâce à ksb496, j'ai réussi à faire cette tâche, mais dans mon cas, je dois changer certains codes pour qu'ils fonctionnent comme prévu. J'ai pensé que cela pourrait peut-être aider les autres, alors j'ai décidé de partager (avec deux ans de retard :D).

J'avais un tampon RGB rempli par un récupérateur d'échantillons directshow dont j'avais besoin pour prendre une vidéo. RGB en YUV la conversion d'une réponse donnée n'a pas fait le travail pour moi. Je l'ai fait comme ça:

int stride = m_width * 3;
int index = 0;
for (int y = 0; y < m_height; y++) {
    for (int x = 0; x < stride; x++) {
        int j = (size - ((y + 1)*stride)) + x;
        m_rgbpic->data[0][j] = data[index];
        ++index;
    }
}

data variable voici mon RGB tampon (simple BYTE*) et size est data taille du tampon en octets. C'est commencer à remplir RGBAVFrame de bas à gauche en haut à droite.

L'autre chose est que ma version de FFMPEG n'avait pas av_packet_rescale_ts fonction. C'est la dernière version mais les documents FFMPEG n'ont pas dit que cette fonction était obsolète, je suppose que cela pourrait être le cas uniquement pour Windows. Quoi qu'il en soit, j'ai utilisé av_rescale_q qui fait le même travail. comme ça :

AVPacket pkt;
pkt.pts = av_rescale_q(pkt.pts, { 1, 25 }, m_stream->time_base);

Et la dernière chose, en utilisant cette conversion de format, j'avais besoin de changer mon swsContext en BGR24 au lieu de RGB24 comme ça :

m_convert_ctx = sws_getContext(width, height, AV_PIX_FMT_BGR24, width, height,
        AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
0
HMD