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

Le pseudo-code 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 (1920 x 1080), 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 d'entre eux n'a résolu spécifiquement ce que je veux exactement, car beaucoup d'entre eux concernent l'exécution de l'encodeur avec des fichiers images existants sur le disque, tandis que d'autres fournissent des solutions pour d'autres langages de programmation tels que Python href = "https://stackoverflow.com/questions/13294919/can-you-stream-images-to-ffmpeg-to-construct-a-video-instead-of-saving-them-t"> ici vous pouvez trouver une solution entièrement satisfaisante pour cette plate-forme).

Le pseudocode de ce que je voudrais 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();

Selon ce que j'ai lu sur des sujets connexes, l'API C ++ x264 pourrait peut-être 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 obligé à abandonner cette possibilité en tant que simple programmeur non professionnel que je suis (je le prends comme un passe-temps et malheureusement je ne peux pas gaspiller tant de temps à 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 d'une manière ou d'une autre à transférer les données d'image de chaque itération (stockées en B) vers l'encodeur, permettant l'ajout de chaque image (c'est-à-dire ne pas "fermer" le fichier vidéo à écrire) jusqu'à la dernière image, de sorte que plus d'images puissent être ajoutées jusqu'à atteindre la N-iè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 «attendre» l'encodeur pour plus d'images. Ensuite, appelez à nouveau ffmpeg pour ajouter la deuxième image et faire à nouveau "attendre" l'encodeur pour 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.

Modifier 1:

Comme suggéré dans les réponses, j'ai documenté les tubes nommés et essayé de les utiliser dans mon code. Tout d'abord, il faut remarquer que je travaille avec Cygwin, donc mes tubes 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 précédente WriteToFile, 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 la mise en mémoire tampon du tube.

Puis 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é à la boucle lorsque i = 0 à 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 tube, mais ce n'est pas le cas. Il semble qu'ils ne puissent pas être connectés via le tuyau, mais je n'ai pas pu trouver de documentation supplémentaire pour résoudre ce problème. Toute suggestion?

24
ksb496 29 déc. 2015 à 15:41

4 réponses

Meilleure réponse

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 dans mon but spécifique, grâce aux informations utiles que certains utilisateurs ont fournies sur ce site et d'autres, ainsi que certains Exemples de documentation de FFmpeg. Par souci d'illustration, les détails seront présentés ci-après.

Tout d'abord, la bibliothèque C libx264 a été compilée et, après cela, celle de FFmpeg avec les options de configuration --enable-gpl --enable-libx264. Passons maintenant au codage. La partie pertinente du code qui a atteint l'objectif demandé 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 les besoins de l'exposition, supposons que nous ayons à la place un pointeur vers une structure de la forme uint8_t [3] Matrix (int, int), dont la manière d'accéder aux valeurs de couleur des pixels pour une coordonnée donnée (x, y) est Matrice (x, y) -> Rouge, Matrice (x, y) -> Vert et Matrice (x, y) -> Bleu, 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 déroutantes, 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 faisait que la taille de sortie était beaucoup plus grande que celle obtenue via l'outil de ligne de commande binaire 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 faut remarquer que lors du codage, il existe deux types d'horodatages: l'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 la trame puissent être attribuées à l'aide d'une unité de référence personnalisée (avec la seule restriction qu'elles doivent être espacées de manière égale si l'on veut un FPS constant), donc on peut prendre par exemple le numéro de trame comme nous 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 ou non des trames B (dans le second cas, les valeurs PTS et DTS de 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). Consultez cette 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 profondeur le format de l'en-tête et de la bande-annonce et la durée les tampons sont correctement fixés.

29
Community 23 mai 2017 à 12:26

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 capteur d'échantillons directshow dont j'avais besoin pour prendre une vidéo. La conversion de RGB en YUV à partir de la réponse donnée n'a pas fonctionné à ma place. 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;
    }
}

La variable data ici est mon RGB tampon (simple BYTE*) et size est la taille du tampon data en octets. Il commence à remplir RGB AVFrame de bas à gauche en haut à droite.

L'autre chose est que ma version de FFMPEG n'avait pas de fonction av_packet_rescale_ts. C'est la dernière version, mais les documents FFMPEG ne disent pas que cette fonction est obsolète, je suppose que cela pourrait être le cas pour Windows uniquement. Quoi qu'il en soit, j'ai utilisé av_rescale_q à la place 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, je devais changer mon swsContext en BGR24 au lieu de RGB24 comme ceci:

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 12 mai 2018 à 10:07

Merci pour votre excellent travail, @ ksb496!

Une amélioration mineure:

c=avcodec_alloc_context3(codec);

Devrait être mieux écrit comme suit:

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 à être déployée sur GitHub: https://github.com/apc-llc/moviemaker-cpp.git

3
Dmitry Mikushin 4 juin 2018 à 13:35

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

0
HMD 19 août 2020 à 14:25