FFmpeg libav manual


Longtemps que je cherchais un livre qui serait mĂąchĂ© Ă  utiliser FFmpeg -comme bibliothĂšque connue sous le nom libav (le nom signifie lib raire un udio v idĂ©o ). TrouvĂ© un manuel " Comment Ă©crire un lecteur vidĂ©o et tenir en moins de mille lignes ." Malheureusement, les informations y sont obsolĂštes, j'ai donc dĂ» crĂ©er un manuel par moi-mĂȘme.

La plupart du code sera en C, mais ne vous inquiĂ©tez pas: vous comprendrez tout facilement et pourrez l'appliquer dans votre langue prĂ©fĂ©rĂ©e. FFmpeg libav a beaucoup de liaisons avec de nombreux langages (dont Python et Go). Mais mĂȘme si votre langue n'a pas de compatibilitĂ© directe, vous pouvez toujours vous attacher via ffi (voici un exemple avecLua ).

Commençons par une courte digression sur ce que sont la vidéo, l'audio, les codecs et les conteneurs. Ensuite, nous passons au cours accéléré sur l'utilisation de la ligne de commande FFmpeg, et écrivons enfin le code. N'hésitez pas à aller directement à la section «Le chemin épineux de l'apprentissage FFmpeg libav».

Il existe une opinion (et pas seulement la mienne) selon laquelle le streaming de vidĂ©os sur Internet a dĂ©jĂ  pris le relais de la tĂ©lĂ©vision traditionnelle. Quoi qu'il en soit, FFmpeg libav vaut vraiment la peine d'ĂȘtre explorĂ©.

Table des matiĂšres


Logiciel EDISON - développement web
EDISON.

, , .

! ;-)

↑


— , ! ↑


Si la séquence d'images est modifiée à une fréquence donnée (disons, 24 images par seconde), une illusion de mouvement est créée. C'est l'idée principale de la vidéo: une série d'images (cadres) se déplaçant à une vitesse donnée.

Illustration de 1886.

L'audio est ce que vous entendez! ↑


Bien que la vidéo silencieuse puisse provoquer une grande variété de sentiments, l'ajout de son augmente considérablement le degré de plaisir.

Le son est une onde vibratoire se propageant dans l'air ou dans tout autre milieu de transmission (tel que gaz, liquide ou solide).

Dans un systÚme audio numérique, un microphone convertit le son en un signal électrique analogique. Ensuite, un convertisseur analogique-numérique ( ADC ) - qui utilise généralement la modulation par impulsions codées ( PCM ) - convertit un signal analogique en signal numérique.


Codec - compression des donnĂ©es ↑


Un codec est un circuit électronique ou un logiciel qui comprime ou décompresse l'audio / vidéo numérique. Il convertit l'audio / vidéo numérique brut (non compressé) en un format compressé (ou vice versa).

Mais si nous décidons de regrouper des millions d'images dans un fichier et de l'appeler film, nous pouvons obtenir un fichier énorme. Calculons:

Supposons que nous crĂ©ons une vidĂ©o avec une rĂ©solution de 1080 × 1920 (hauteur × largeur). Nous dĂ©pensons 3 octets par pixel (le point minimum sur l'Ă©cran) pour le codage couleur (couleur 24 bits, ce qui nous donne 16 777 216 couleurs diffĂ©rentes). Cette vidĂ©o fonctionne Ă  une vitesse de 24 images par seconde, la durĂ©e totale de 30 minutes.

toppf = 1080 * 1920 //    
cpp = 3 //  
tis = 30 * 60 //   
fps = 24 //   

required_storage = tis * fps * toppf * cpp

Cette vidéo nécessitera environ 250,28 Go de mémoire, soit 1,11 Go / s! C'est pourquoi vous devez utiliser un codec.

Un conteneur est un moyen pratique de stocker de l'audio / vidĂ©o ↑


Le format conteneur (wrapper) est un format de métafichier dont la spécification décrit la façon dont divers éléments de données et de métadonnées coexistent dans un fichier informatique.

Il s'agit d'un fichier unique contenant tous les flux (principalement audio et vidéo), assurant la synchronisation, contenant des métadonnées communes (telles que le titre, la résolution), etc.

En rÚgle générale, le format de fichier est déterminé par son extension: par exemple, video.webm est trÚs probablement une vidéo utilisant le conteneur webm.


Ligne de commande FFmpeg ↑


Solution multiplateforme autonome pour l'enregistrement, la conversion et le streaming audio / vidéo.

Pour travailler avec le multimĂ©dia, nous avons un outil incroyable - une bibliothĂšque appelĂ©e FFmpeg . MĂȘme si vous ne l'utilisez pas dans votre code de programme, vous l'utilisez toujours (utilisez-vous Chrome?).

La bibliothĂšque possĂšde un programme de console pour entrer une ligne de commande appelĂ©e ffmpeg (en petites lettres, contrairement au nom de la bibliothĂšque elle-mĂȘme). Il s'agit d'un binaire simple et puissant. Par exemple, vous pouvez convertir de mp4 en avi en tapant simplement cette commande:

$ ffmpeg -i input.mp4 output.avi

Nous venons de remixer - converti d'un conteneur Ă  un autre. Techniquement, FFmpeg peut Ă©galement transcoder, mais plus Ă  ce sujet plus tard.

Outil de ligne de commande FFmpeg 101 ↑


FFmpeg a une documentation oĂč tout est parfaitement expliquĂ© comment ce qui fonctionne.

SchĂ©matiquement, le programme de ligne de commande FFmpeg attend que le format d'argument suivant fasse son travail - ffmpeg {1} {2} -i {3} {4} {5}oĂč:

{1} - paramĂštres globaux
{2} - paramÚtres du fichier d'entrée
{3} - URL entrante
{4} - paramĂštres du fichier de sortie
{5} - sortants Les

parties d' URL {2}, {3}, {4}, {5} spécifient autant d'arguments que nécessaire. Il est plus facile de comprendre le format de passage des arguments à l'aide d'un exemple:

AVERTISSEMENT: un fichier par référence pÚse 300 Mo

$ wget -O bunny_1080p_60fps.mp4 http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_60fps_normal.mp4

$ ffmpeg \
-y \ #  
-c: libfdk_aac -c: v libx264 \ #  
-i bunny_1080p_60fps.mp4 \ #  URL
-c: v libvpx-vp9 -c: libvorbis \ #  
bunny_1080p_60fps_vp9.webm #  URL

Cette commande prend un fichier mp4 entrant contenant deux flux (audio codé à l'aide du codec aac et vidéo codé à l'aide du codec h264), et le convertit en webm, en modifiant également les codecs audio et vidéo.

Si vous simplifiez la commande ci-dessus, vous devez considérer que FFmpeg acceptera les valeurs par défaut à votre place. Par exemple, si vous tapez simplement

ffmpeg -i input.avi output.mp4

quel codec audio / vidéo utilise-t-il pour créer output.mp4?

Werner Robitz a écrit un guide de codage et d'édition pour la lecture / exécution avec FFmpeg.

OpĂ©rations vidĂ©o de base ↑


Lorsque nous travaillons avec de l'audio / vidéo, nous effectuons généralement un certain nombre de tùches liées au multimédia.

Transcodage (transcodage) ↑




Qu'Est-ce que c'est? Processus de conversion de streaming ou audio ou vidĂ©o (ou les deux en mĂȘme temps) d'un codec Ă  un autre. Le format de fichier (conteneur) ne change pas.

Pour quoi? Il arrive que certains appareils (téléviseurs, smartphones, consoles, etc.) ne prennent pas en charge le format audio / vidéo X, mais prennent en charge le format audio / vidéo Y. Ou, les codecs plus récents sont préférables car ils offrent un meilleur taux de compression.

Comment? Convertissez, par exemple, la vidéo H264 (AVC) en H265 (HEVC):

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-c:v libx265 \
bunny_1080p_60fps_h265.mp4

Transmultiplexage ↑



Qu'Est-ce que c'est? Conversion d'un format (conteneur) Ă  un autre.

Pour quoi? Il arrive que certains appareils (téléviseurs, smartphones, consoles, etc.) ne prennent pas en charge le format de fichier X, mais prennent en charge le format de fichier Y. Ou, les conteneurs plus récents, contrairement aux anciens, fournissent les fonctions modernes requises.

Comment? Convertissez un mp4 en webm:

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-c copy \ # just saying to ffmpeg to skip encoding
bunny_1080p_60fps.webm

Transrating ↑



Qu'Est-ce que c'est? Modifiez le débit de données ou créez une autre vue.

Pour quoi? L'utilisateur peut regarder votre vidĂ©o Ă  la fois sur un rĂ©seau 2G sur un smartphone basse consommation et via une connexion Internet en fibre optique sur un tĂ©lĂ©viseur 4K. Par consĂ©quent, vous devez proposer plusieurs options pour lire la mĂȘme vidĂ©o avec des dĂ©bits de donnĂ©es diffĂ©rents.

Comment? produit une lecture à un débit compris entre 3856K et 2000K.

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-minrate 964K -maxrate 3856K -bufsize 2000K \
bunny_1080p_60fps_transrating_964_3856.mp4

En rÚgle générale, la conversion est effectuée conjointement avec le recalibrage. Werner Robitz a écrit un autre article obligatoire sur le contrÎle de vitesse FFmpeg.

Transizing (recalibrage) ↑



Qu'Est-ce que c'est? Changement de résolution. Comme indiqué ci-dessus, le transsizing est souvent effectué simultanément avec le transrating.

Pour quoi? Pour les mĂȘmes raisons que pour le transfert.

Comment? Réduisez la résolution de 1080 à 480:

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-vf scale=480:-1 \
bunny_1080p_60fps_transsizing_480.mp4

Bonus: streaming adaptatif ↑



Qu'Est-ce que c'est? Création de nombreuses autorisations (débits binaires) et division des médias en parties et leur transmission via le protocole http.

Pour quoi? Dans le but de fournir un multimĂ©dia flexible, qui peut ĂȘtre visualisĂ© mĂȘme sur un smartphone Ă©conomique, mĂȘme sur un plasma 4K, afin qu'il puisse ĂȘtre facilement mis Ă  l'Ă©chelle et dĂ©ployĂ© (mais cela peut ajouter un retard).

Comment? Créez un WebM réactif à l'aide de DASH:

# video streams
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 160x90 -b:v 250k -keyint_min 150 -g 150 -an -f webm -dash 1 video_160x90_250k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 320x180 -b:v 500k -keyint_min 150 -g 150 -an -f webm -dash 1 video_320x180_500k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 640x360 -b:v 750k -keyint_min 150 -g 150 -an -f webm -dash 1 video_640x360_750k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 640x360 -b:v 1000k -keyint_min 150 -g 150 -an -f webm -dash 1 video_640x360_1000k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 1280x720 -b:v 1500k -keyint_min 150 -g 150 -an -f webm -dash 1 video_1280x720_1500k.webm

# audio streams
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:a libvorbis -b:a 128k -vn -f webm -dash 1 audio_128k.webm

# the DASH manifest
$ ffmpeg \
 -f webm_dash_manifest -i video_160x90_250k.webm \
 -f webm_dash_manifest -i video_320x180_500k.webm \
 -f webm_dash_manifest -i video_640x360_750k.webm \
 -f webm_dash_manifest -i video_640x360_1000k.webm \
 -f webm_dash_manifest -i video_1280x720_500k.webm \
 -f webm_dash_manifest -i audio_128k.webm \
 -c copy -map 0 -map 1 -map 2 -map 3 -map 4 -map 5 \
 -f webm_dash_manifest \
 -adaptation_sets "id=0,streams=0,1,2,3,4 id=1,streams=5" \
 manifest.mpd

PS: J'ai tiré cet exemple des instructions pour jouer à Adaptive WebM à l'aide de DASH .

Aller au-delà ↑


Il n'y a pas d'autres utilisations pour FFmpeg. Je l'utilise avec iMovie pour crĂ©er / Ă©diter des vidĂ©os YouTube. Et, bien sĂ»r, rien ne vous empĂȘche de l'utiliser professionnellement.

Le chemin Ă©pineux de l'apprentissage FFmpeg libav ↑

N'est-ce pas étonnant de temps en temps qui soit perçu par l'ouïe et la vue?

Biologiste David Robert Jones
FFmpeg est extrĂȘmement utile comme outil de ligne de commande pour effectuer des opĂ©rations importantes avec des fichiers multimĂ©dias. Peut-ĂȘtre qu'il peut Ă©galement ĂȘtre utilisĂ© dans des programmes?

FFmpeg se compose de plusieurs bibliothĂšques qui peuvent ĂȘtre intĂ©grĂ©es dans nos propres programmes. GĂ©nĂ©ralement, lorsque vous installez FFmpeg, toutes ces bibliothĂšques sont automatiquement installĂ©es. Je ferai rĂ©fĂ©rence Ă  un ensemble de ces bibliothĂšques comme FFmpeg libav .

Le titre de la section est un hommage à la série de Zed Shaw, The Thorny Path of Learning [...] , en particulier son livre The Thorny Path of Learning C.

Chapitre 0 - Le simple monde bonjour ↑


Dans notre Hello World , vous n'accueillerez vraiment pas le monde en langage console. Au lieu de cela, imprimez les informations suivantes sur la vidéo: format (conteneur), durée, résolution, canaux audio, et enfin, déchiffrez certaines images et enregistrez-les sous forme de fichiers image.

Architecture libav FFmpeg ↑


Mais avant de commencer à écrire le code, voyons comment l'architecture libav FFmpeg fonctionne en général et comment ses composants interagissent avec les autres.

Voici un schéma du processus de décodage vidéo:

Tout d'abord, le fichier multimĂ©dia est chargĂ© dans un composant appelĂ© AVFormatContext (le conteneur vidĂ©o est Ă©galement un format). En fait, il ne tĂ©lĂ©charge pas entiĂšrement le fichier entier: souvent seul l'en-tĂȘte est lu.

Une fois que vous avez tĂ©lĂ©chargĂ© l'en- tĂȘte minimum de notre conteneur , vous pouvez accĂ©der Ă  ses flux (ils peuvent ĂȘtre reprĂ©sentĂ©s comme des donnĂ©es audio et vidĂ©o Ă©lĂ©mentaires). Chaque flux sera disponible dans le composant AVStream .

Supposons que notre vidéo comporte deux flux: audio codé à l'aide du codec AAC et vidéo codé à l'aide du codec H264 ( AVC ). De chaque flux, nous pouvons extraire des éléments de données appelés paquetsqui sont chargés dans des composants appelés AVPacket .

Les données à l'intérieur des paquets sont toujours codées (compressées), et pour décoder les paquets, nous devons les transmettre à un AVCodec spécifique .

AVCodec les dĂ©code en AVFrame , Ă  la suite de quoi ce composant nous donne une trame non compressĂ©e. Notez que la terminologie et le processus sont les mĂȘmes pour les flux audio et vidĂ©o.

Exigences ↑


Puisqu'il y a parfois des problÚmes lors de la compilation ou de l'exécution d'exemples, nous utiliserons Docker comme environnement de développement / d'exécution. Nous utiliserons également une vidéo avec un gros lapin , donc si vous ne l'avez pas sur votre ordinateur local, lancez simplement la commande make fetch_small_bunny_video dans la console .

En fait, le code ↑


TLDR montrez-moi un exemple de code exécutable, bro:

$ make run_hello

Nous allons omettre certains détails, mais ne vous inquiétez pas: le code source est disponible sur github.

Nous allons allouer de la mémoire pour le composant AVFormatContext , qui contiendra des informations sur le format (conteneur).

AVFormatContext *pFormatContext = avformat_alloc_context();

Maintenant, nous allons ouvrir le fichier, lire son en-tĂȘte et remplir AVFormatContext avec des informations de format minimal (notez que les codecs ne s'ouvrent gĂ©nĂ©ralement pas). Pour ce faire, utilisez la fonction avformat_open_input . Il attend AVFormatContext , un nom de fichier et deux arguments facultatifs: AVInputFormat (si vous passez NULL, FFmpeg dĂ©terminera le format) et AVDictionary (qui sont des options de dĂ©multiplexeur).

avformat_open_input(&pFormatContext, filename, NULL, NULL);

Vous pouvez également imprimer le nom du format et la durée du support:

printf("Format %s, duration %lld us", pFormatContext->iformat->long_name, pFormatContext->duration);

Pour accéder aux flux, nous devons lire les données des médias. Cela se fait par la fonction avformat_find_stream_info . Maintenant pFormatContext-> nb_streams contiendra le nombre de threads, et pFormatContext-> streams [i] nous donnera le i Úme flux consécutif ( AVStream ).

avformat_find_stream_info(pFormatContext,  NULL);

Passons en revue la boucle dans tous les threads:

for(int i = 0; i < pFormatContext->nb_streams; i++) {
  //
}

Pour chaque flux, nous allons enregistrer AVCodecParameters , qui décrit les propriétés du codec utilisé par le i- Úme flux:

AVCodecParameters *pLocalCodecParameters = pFormatContext->streams[i]->codecpar;


En utilisant les propriétés des codecs, nous pouvons trouver celui correspondant en demandant la fonction avcodec_find_decoder , nous pouvons également trouver le décodeur enregistré pour l'identifiant du codec et renvoyer AVCodec , un composant qui sait coder et décoder le flux:

AVCodec *pLocalCodec = avcodec_find_decoder(pLocalCodecParameters->codec_id);

Nous pouvons maintenant imprimer les informations du codec:

// specific for video and audio
if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_VIDEO) {
  printf("Video Codec: resolution %d x %d", pLocalCodecParameters->width, pLocalCodecParameters->height);
} else if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_AUDIO) {
  printf("Audio Codec: %d channels, sample rate %d", pLocalCodecParameters->channels, pLocalCodecParameters->sample_rate);
}
// general
printf("\tCodec %s ID %d bit_rate %lld", pLocalCodec->long_name, pLocalCodec->id, pCodecParameters->bit_rate);

En utilisant le codec, nous allouons de la mémoire à AVCodecContext , qui contiendra le contexte de notre processus de décodage / codage. Mais alors vous devez remplir ce contexte de codec avec les paramÚtres CODEC - nous le faisons en utilisant avcodec_parameters_to_context .

AprĂšs avoir rempli le contexte du codec, vous devez ouvrir le codec. Nous appelons la fonction avcodec_open2 et ensuite nous pouvons l'utiliser:

AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec);
avcodec_parameters_to_context(pCodecContext, pCodecParameters);
avcodec_open2(pCodecContext, pCodec, NULL);

Nous allons maintenant lire les paquets du flux et les décoder en trames, mais nous devons d'abord allouer de la mémoire aux deux composants ( AVPacket et AVFrame ).

AVPacket *pPacket = av_packet_alloc();
AVFrame *pFrame = av_frame_alloc();

Nourrissons nos packages Ă  partir des flux de la fonction av_read_frame pendant qu'elle a les packages:

while(av_read_frame(pFormatContext, pPacket) >= 0) {
  //...
}

Nous allons maintenant envoyer le paquet de données brutes (trame compressée) au décodeur via le contexte du codec en utilisant la fonction avcodec_send_packet :

avcodec_send_packet(pCodecContext, pPacket);

Et rĂ©cupĂ©rons une trame de donnĂ©es brutes (une trame non compressĂ©e) du dĂ©codeur via le mĂȘme contexte de codec en utilisant la fonction avcodec_receive_frame :

avcodec_receive_frame(pCodecContext, pFrame);

Nous pouvons imprimer le numéro de trame, PTS, DTS, le type de trame, etc.:

printf(
    "Frame %c (%d) pts %d dts %d key_frame %d [coded_picture_number %d, display_picture_number %d]",
    av_get_picture_type_char(pFrame->pict_type),
    pCodecContext->frame_number,
    pFrame->pts,
    pFrame->pkt_dts,
    pFrame->key_frame,
    pFrame->coded_picture_number,
    pFrame->display_picture_number
);

Et enfin, nous pouvons enregistrer notre cadre dĂ©codĂ© dans une simple image grise. Le processus est trĂšs simple: nous utiliserons pFrame-> data , oĂč l'index est associĂ© aux espaces colorimĂ©triques Y , Cb et Cr . SĂ©lectionnez simplement 0 (Y) pour enregistrer notre image grise:

save_gray_frame(pFrame->data[0], pFrame->linesize[0], pFrame->width, pFrame->height, frame_filename);

static void save_gray_frame(unsigned char *buf, int wrap, int xsize, int ysize, char *filename)
{
    FILE *f;
    int i;
    f = fopen(filename,"w");
    // writing the minimal required header for a pgm file format
    // portable graymap format -> https://en.wikipedia.org/wiki/Netpbm_format#PGM_example
    fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255);

    // writing line by line
    for (i = 0; i < ysize; i++)
        fwrite(buf + i * wrap, 1, xsize, f);
    fclose(f);
}

Et le tour est joué! Nous avons maintenant une image en niveaux de gris de 2 Mo:


Chapitre 1 - Synchroniser l'audio et la vidĂ©o ↑

Être dans le jeu, c'est quand un jeune dĂ©veloppeur JS Ă©crit un nouveau lecteur vidĂ©o MSE.
Avant de commencer à écrire le code de transcodage, parlons de la synchronisation ou de la façon dont le lecteur vidéo trouve le bon moment pour lire une image.

Dans l'exemple précédent, nous avons enregistré plusieurs images:


lorsque nous concevons un lecteur vidéo, nous devons lire chaque image à un certain rythme, sinon il est difficile d'apprécier la vidéo, car elle est trop rapide ou trop lente.

Par consĂ©quent, nous devons dĂ©finir une logique pour une lecture fluide de chaque image. A cet Ă©gard, chaque trame a un repĂšre de reprĂ©sentation temporelle ( PTS - de p rĂ©sentation t ime s tamp), qui est un nombre croissant pris en compte dans la variablebase de temps , qui est un nombre rationnel (oĂč le dĂ©nominateur est connu sous le nom d'Ă©chelle de temps - Ă©chelle de temps ) divisĂ© par la frĂ©quence d' images ( fps ).

C'est plus facile à comprendre avec des exemples. Simulons quelques scénarios.

Pour fps = 60/1 et base de temps = 1/60000, chaque PTS augmentera l' Ă©chelle de temps / fps = 1000 , de sorte que le temps PTS rĂ©el pour chaque trame peut ĂȘtre (Ă  condition qu'il commence Ă  0):

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 1000, PTS_TIME = PTS * timebase = 0.016
frame=2, PTS = 2000, PTS_TIME = PTS * timebase = 0.033

Presque le mĂȘme scĂ©nario, mais avec une Ă©chelle de temps Ă©gale Ă  1/60:

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 1, PTS_TIME = PTS * timebase = 0.016
frame=2, PTS = 2, PTS_TIME = PTS * timebase = 0.033
frame=3, PTS = 3, PTS_TIME = PTS * timebase = 0.050

Pour fps = 25/1 et base de temps = 1/75, chaque PTS augmentera l' Ă©chelle de temps / fps = 3 , et le temps PTS peut ĂȘtre:

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 3, PTS_TIME = PTS * timebase = 0.04
frame=2, PTS = 6, PTS_TIME = PTS * timebase = 0.08
frame=3, PTS = 9, PTS_TIME = PTS * timebase = 0.12
...
frame=24, PTS = 72, PTS_TIME = PTS * timebase = 0.96
...
frame=4064, PTS = 12192, PTS_TIME = PTS * timebase = 162.56

Maintenant, avec pts_time, nous pouvons trouver un moyen de visualiser cela en synchronisation avec le son de pts_time ou avec l'horloge systĂšme. FFmpeg libav fournit ces informations via son API:

fps = AVStream->avg_frame_rate
tbr = AVStream->r_frame_rate
tbn = AVStream->time_base


Par simple curiosité, les images que nous avons enregistrées ont été envoyées dans l'ordre DTS (images: 1, 6, 4, 2, 3, 5), mais reproduites dans l'ordre PTS (images: 1, 2, 3, 4, 5). Notez également combien d' images B moins chÚres sont comparées aux images P ou I :

LOG: AVStream->r_frame_rate 60/1
LOG: AVStream->time_base 1/60000
...
LOG: Frame 1 (type=I, size=153797 bytes) pts 6000 key_frame 1 [DTS 0]
LOG: Frame 2 (type=B, size=8117 bytes) pts 7000 key_frame 0 [DTS 3]
LOG: Frame 3 (type=B, size=8226 bytes) pts 8000 key_frame 0 [DTS 4]
LOG: Frame 4 (type=B, size=17699 bytes) pts 9000 key_frame 0 [DTS 2]
LOG: Frame 5 (type=B, size=6253 bytes) pts 10000 key_frame 0 [DTS 5]
LOG: Frame 6 (type=P, size=34992 bytes) pts 11000 key_frame 0 [DTS 1]

Chapitre 2 - Remultiplexage ↑


Remultiplexage (réarrangement, remuxage) - la transition d'un format (conteneur) à un autre. Par exemple, nous pouvons facilement remplacer la vidéo MPEG-4 par MPEG-TS en utilisant FFmpeg:

ffmpeg input.mp4 -c copy output.ts

Le fichier MP4 sera démultiplexé, tandis que le fichier ne sera pas décodé ou encodé ( copie -c ), et, à la fin, nous obtenons le fichier mpegts. Si vous ne spécifiez pas le format -f , ffmpeg essaiera de le deviner en fonction de l'extension du fichier.

L'utilisation générale de FFmpeg ou libav suit un tel modÚle / architecture ou flux de travail:

  • niveau de protocole - accepter les donnĂ©es d'entrĂ©e (par exemple, un fichier, mais il peut Ă©galement s'agir d'un tĂ©lĂ©chargement rtmp ou HTTP)
  • — , , ,
  • —
  • — (, ),
  • 
 :
  • — ( )
  • — ( ) ( )
  • — , , ( , , )


(Ce graphique est trÚs inspiré par le travail de Leixiaohua et Slhck ) Créons

maintenant un exemple utilisant libav pour fournir le mĂȘme effet que lors de l'exĂ©cution de cette commande:

ffmpeg input.mp4 -c copy output.ts

Nous allons lire depuis l'entrée ( input_format_context ) et la changer en une autre sortie ( output_format_context ):

AVFormatContext *input_format_context = NULL;
AVFormatContext *output_format_context = NULL;

Habituellement, nous commençons par allouer de la mémoire et ouvrir le format d'entrée. Pour ce cas spécifique, nous allons ouvrir le fichier d'entrée et allouer de la mémoire au fichier de sortie:

if ((ret = avformat_open_input(&input_format_context, in_filename, NULL, NULL)) < 0) {
  fprintf(stderr, "Could not open input file '%s'", in_filename);
  goto end;
}
if ((ret = avformat_find_stream_info(input_format_context, NULL)) < 0) {
  fprintf(stderr, "Failed to retrieve input stream information");
  goto end;
}

avformat_alloc_output_context2(&output_format_context, NULL, NULL, out_filename);
if (!output_format_context) {
  fprintf(stderr, "Could not create output context\n");
  ret = AVERROR_UNKNOWN;
  goto end;
}

Nous ne remultiplexerons que les flux vidéo, audio et sous-titres. Par conséquent, nous fixons les flux que nous utiliserons dans un tableau d'indices:

number_of_streams = input_format_context->nb_streams;
streams_list = av_mallocz_array(number_of_streams, sizeof(*streams_list));

Immédiatement aprÚs avoir alloué la mémoire nécessaire, nous devons parcourir tous les flux et pour chacun, nous devons créer un nouveau flux de sortie dans notre contexte du format de sortie à l'aide de la fonction avformat_new_stream . Veuillez noter que nous signalons tous les flux qui ne sont pas vidéo, audio ou sous-titres afin que nous puissions les ignorer.

for (i = 0; i < input_format_context->nb_streams; i++) {
  AVStream *out_stream;
  AVStream *in_stream = input_format_context->streams[i];
  AVCodecParameters *in_codecpar = in_stream->codecpar;
  if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
      in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
      in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
    streams_list[i] = -1;
    continue;
  }
  streams_list[i] = stream_index++;
  out_stream = avformat_new_stream(output_format_context, NULL);
  if (!out_stream) {
    fprintf(stderr, "Failed allocating output stream\n");
    ret = AVERROR_UNKNOWN;
    goto end;
  }
  ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
  if (ret < 0) {
    fprintf(stderr, "Failed to copy codec parameters\n");
    goto end;
  }
}

Créez maintenant le fichier de sortie:

if (!(output_format_context->oformat->flags & AVFMT_NOFILE)) {
  ret = avio_open(&output_format_context->pb, out_filename, AVIO_FLAG_WRITE);
  if (ret < 0) {
    fprintf(stderr, "Could not open output file '%s'", out_filename);
    goto end;
  }
}

ret = avformat_write_header(output_format_context, NULL);
if (ret < 0) {
  fprintf(stderr, "Error occurred when opening output file\n");
  goto end;
}

AprÚs cela, vous pouvez copier des flux, package par package, de nos flux d'entrée vers nos flux de sortie. Cela se produit dans une boucle, tant qu'il existe des packages ( av_read_frame ), pour chaque package, vous devez recalculer PTS et DTS pour enfin l'écrire ( av_interleaved_write_frame ) dans notre contexte du format de sortie.

while (1) {
  AVStream *in_stream, *out_stream;
  ret = av_read_frame(input_format_context, &packet);
  if (ret < 0)
    break;
  in_stream  = input_format_context->streams[packet.stream_index];
  if (packet.stream_index >= number_of_streams || streams_list[packet.stream_index] < 0) {
    av_packet_unref(&packet);
    continue;
  }
  packet.stream_index = streams_list[packet.stream_index];
  out_stream = output_format_context->streams[packet.stream_index];
  /* copy packet */
  packet.pts = av_rescale_q_rnd(packet.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
  packet.dts = av_rescale_q_rnd(packet.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
  packet.duration = av_rescale_q(packet.duration, in_stream->time_base, out_stream->time_base);
  // https://ffmpeg.org/doxygen/trunk/structAVPacket.html#ab5793d8195cf4789dfb3913b7a693903
  packet.pos = -1;

  //https://ffmpeg.org/doxygen/trunk/group__lavf__encoding.html#ga37352ed2c63493c38219d935e71db6c1
  ret = av_interleaved_write_frame(output_format_context, &packet);
  if (ret < 0) {
    fprintf(stderr, "Error muxing packet\n");
    break;
  }
  av_packet_unref(&packet);
}

Pour terminer, nous devons écrire la bande-annonce du flux dans le fichier multimédia de sortie à l'aide de la fonction av_write_trailer :

av_write_trailer(output_format_context);

Nous sommes maintenant prĂȘts Ă  tester le code. Et le premier test sera la conversion du format (conteneur vidĂ©o) de MP4 en fichier vidĂ©o MPEG-TS. Fondamentalement, nous crĂ©ons une ligne de commande ffmpeg input.mp4 -c pour copier output.ts en utilisant libav.

make run_remuxing_ts

Ça marche! Ne me crois pas ?! VĂ©rifiez avec ffprobe :

ffprobe -i remuxed_small_bunny_1080p_60fps.ts

Input #0, mpegts, from 'remuxed_small_bunny_1080p_60fps.ts':
  Duration: 00:00:10.03, start: 0.000000, bitrate: 2751 kb/s
  Program 1
    Metadata:
      service_name    : Service01
      service_provider: FFmpeg
    Stream #0:0[0x100]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 60 fps, 60 tbr, 90k tbn, 120 tbc
    Stream #0:1[0x101]: Audio: ac3 ([129][0][0][0] / 0x0081), 48000 Hz, 5.1(side), fltp, 320 kb/s

Pour résumer ce que nous avons fait, nous pouvons maintenant revenir à notre idée originale du fonctionnement de libav. Mais nous avons manqué une partie du codec, qui est affiché dans le diagramme.


Avant de terminer ce chapitre, je voudrais montrer une partie si importante du processus de remultiplexage, oĂč vous pouvez passer des paramĂštres au multiplexeur. Supposons que vous souhaitiez fournir le format MPEG-DASH, vous devez donc utiliser mp4 fragmentĂ© (parfois appelĂ© fmp4 ) au lieu de MPEG-TS ou MPEG-4 ordinaire.

L'utilisation de la ligne de commande est simple:

ffmpeg -i non_fragmented.mp4 -movflags frag_keyframe+empty_moov+default_base_moof fragmented.mp4

C'est presque aussi simple que cela dans la version libav, nous passons simplement les options lors de l'Ă©criture de l'en-tĂȘte de sortie, juste avant de copier les packages:

AVDictionary* opts = NULL;
av_dict_set(&opts, "movflags", "frag_keyframe+empty_moov+default_base_moof", 0);
ret = avformat_write_header(output_format_context, &opts);

Maintenant, nous pouvons générer ce fichier mp4 fragmenté:

make run_remuxing_fragmented_mp4

Pour vous assurer que tout est juste, vous pouvez utiliser l'incroyable site de l' outil gpac / mp4box.js ou http://mp4parser.com/ pour voir les différences - téléchargez d'abord mp4.

Comme vous pouvez le voir, il a un bloc mdat indivisible - c'est l'endroit oĂč se trouvent les images vidĂ©o et audio. TĂ©lĂ©chargez maintenant le mp4 fragmentĂ© pour voir comment il Ă©tend les blocs mdat:

Chapitre 3 - Transcodage ↑


TLDR montrez-moi le code et l'exécution:

$ make run_transcoding

Nous ignorerons certains détails, mais ne vous inquiétez pas: le code source est disponible sur github.

Dans ce chapitre, nous allons créer un transcodeur minimaliste écrit en C, qui peut convertir des vidéos de H264 en H265 en utilisant les bibliothÚques libav FFmpeg, en particulier libavcodec , libavformat et libavutil .


AVFormatContext est une abstraction pour le format de fichier multimédia, c'est-à-dire pour un conteneur (MKV, MP4, Webm, TS)
AVStream représente chaque type de données pour un format donné (par exemple: audio, vidéo, sous-titres, métadonnées)
AVPacket est un fragment de donnĂ©es compressĂ©es reçues d' AVStream qui peuvent ĂȘtre dĂ©codĂ©es Ă  l'aide d' AVCodec (par exemple : av1, h264, vp9, hevc) gĂ©nĂ©rant des donnĂ©es brutes appelĂ©es AVFrame .

Transmultiplexage ↑


Commençons par une simple conversion, puis chargeons le fichier d'entrée.

// Allocate an AVFormatContext
avfc = avformat_alloc_context();
// Open an input stream and read the header.
avformat_open_input(avfc, in_filename, NULL, NULL);
// Read packets of a media file to get stream information.
avformat_find_stream_info(avfc, NULL);

Configurez maintenant le décodeur. AVFormatContext nous donnera accÚs à tous les composants d' AVStream , et pour chacun desquels nous pouvons obtenir leur AVCodec et créer un AVCodecContext spécifique . Et enfin, nous pouvons ouvrir ce codec pour passer au processus de décodage.

AVCodecContext contient des données de configuration multimédia, telles que le débit de données, la fréquence d'images, la fréquence d'échantillonnage, les canaux, la hauteur et bien d'autres.

for(int i = 0; i < avfc->nb_streams; i++) {
  AVStream *avs = avfc->streams[i];
  AVCodec *avc = avcodec_find_decoder(avs->codecpar->codec_id);
  AVCodecContext *avcc = avcodec_alloc_context3(*avc);
  avcodec_parameters_to_context(*avcc, avs->codecpar);
  avcodec_open2(*avcc, *avc, NULL);
}

Vous devez également préparer le fichier multimédia de sortie pour la conversion. Tout d'abord, allouez de la mémoire pour la sortie AVFormatContext . Créez chaque flux dans le format de sortie. Pour emballer correctement le flux, copiez les paramÚtres du codec depuis le décodeur.

DĂ©finissez l'indicateur AV_CODEC_FLAG_GLOBAL_HEADER , qui indique Ă  l'encodeur qu'il peut utiliser des en-tĂȘtes globaux, et enfin ouvrez le fichier de sortie pour l'Ă©criture et enregistrez les en-tĂȘtes:

avformat_alloc_output_context2(&encoder_avfc, NULL, NULL, out_filename);

AVStream *avs = avformat_new_stream(encoder_avfc, NULL);
avcodec_parameters_copy(avs->codecpar, decoder_avs->codecpar);

if (encoder_avfc->oformat->flags & AVFMT_GLOBALHEADER)
  encoder_avfc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

avio_open(&encoder_avfc->pb, encoder->filename, AVIO_FLAG_WRITE);
avformat_write_header(encoder->avfc, &muxer_opts);

Nous obtenons AVPacket du décodeur, ajustons les horodatages et écrivons le paquet correctement dans le fichier de sortie. Malgré le fait que la fonction av_interleaved_write_frame signale « écrire le cadre », nous enregistrons le package. Nous terminons le processus de permutation en écrivant la bande-annonce du flux dans un fichier.

AVFrame *input_frame = av_frame_alloc();
AVPacket *input_packet = av_packet_alloc();

while(av_read_frame(decoder_avfc, input_packet) >= 0) {
  av_packet_rescale_ts(input_packet, decoder_video_avs->time_base, encoder_video_avs->time_base);
  av_interleaved_write_frame(*avfc, input_packet) < 0));
}

av_write_trailer(encoder_avfc);

Transcodage ↑


Dans la section précédente, il y avait un programme simple pour la conversion, maintenant nous allons ajouter la possibilité d'encoder des fichiers, en particulier, le transcodage vidéo de h264 en h265 .

Une fois le décodeur préparé, mais avant d'organiser le fichier multimédia de sortie, configurez l'encodeur.

  • CrĂ©ez un AVStream vidĂ©o dans l'encodeur avformat_new_stream .
  • Nous utilisons AVCodec avec le nom libx265 , avcodec_find_encoder_by_name .
  • CrĂ©ez un AVCodecContext basĂ© sur le codec crĂ©Ă© avcodec_alloc_context3 .
  • DĂ©finissez les attributs de base pour une session de transcodage et ...
  • ... ouvrez le codec et copiez les paramĂštres du contexte dans le flux ( avcodec_open2 et avcodec_parameters_from_context ).

AVRational input_framerate = av_guess_frame_rate(decoder_avfc, decoder_video_avs, NULL);
AVStream *video_avs = avformat_new_stream(encoder_avfc, NULL);

char *codec_name = "libx265";
char *codec_priv_key = "x265-params";
// we're going to use internal options for the x265
// it disables the scene change detection and fix then
// GOP on 60 frames.
char *codec_priv_value = "keyint=60:min-keyint=60:scenecut=0";

AVCodec *video_avc = avcodec_find_encoder_by_name(codec_name);
AVCodecContext *video_avcc = avcodec_alloc_context3(video_avc);
// encoder codec params
av_opt_set(sc->video_avcc->priv_data, codec_priv_key, codec_priv_value, 0);
video_avcc->height = decoder_ctx->height;
video_avcc->width = decoder_ctx->width;
video_avcc->pix_fmt = video_avc->pix_fmts[0];
// control rate
video_avcc->bit_rate = 2 * 1000 * 1000;
video_avcc->rc_buffer_size = 4 * 1000 * 1000;
video_avcc->rc_max_rate = 2 * 1000 * 1000;
video_avcc->rc_min_rate = 2.5 * 1000 * 1000;
// time base
video_avcc->time_base = av_inv_q(input_framerate);
video_avs->time_base = sc->video_avcc->time_base;

avcodec_open2(sc->video_avcc, sc->video_avc, NULL);
avcodec_parameters_from_context(sc->video_avs->codecpar, sc->video_avcc);

Il est nécessaire d'étendre le cycle de décodage pour transcoder un flux vidéo:

  • Nous envoyons un AVPacket vide au dĂ©codeur ( avcodec_send_packet ).
  • Obtenez l'AVFrame non compressĂ© ( avcodec_receive_frame ).
  • Nous commençons Ă  recoder le cadre brut.
  • Nous envoyons le cadre brut ( avcodec_send_frame ).
  • Nous obtenons une compression basĂ©e sur notre codec AVPacket ( avcodec_receive_packet ).
  • DĂ©finissez l'horodatage ( av_packet_rescale_ts ).
  • Nous Ă©crivons dans le fichier de sortie ( av_interleaved_write_frame ).

AVFrame *input_frame = av_frame_alloc();
AVPacket *input_packet = av_packet_alloc();

while (av_read_frame(decoder_avfc, input_packet) >= 0)
{
  int response = avcodec_send_packet(decoder_video_avcc, input_packet);
  while (response >= 0) {
    response = avcodec_receive_frame(decoder_video_avcc, input_frame);
    if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {
      break;
    } else if (response < 0) {
      return response;
    }
    if (response >= 0) {
      encode(encoder_avfc, decoder_video_avs, encoder_video_avs, decoder_video_avcc, input_packet->stream_index);
    }
    av_frame_unref(input_frame);
  }
  av_packet_unref(input_packet);
}
av_write_trailer(encoder_avfc);

// used function
int encode(AVFormatContext *avfc, AVStream *dec_video_avs, AVStream *enc_video_avs, AVCodecContext video_avcc int index) {
  AVPacket *output_packet = av_packet_alloc();
  int response = avcodec_send_frame(video_avcc, input_frame);

  while (response >= 0) {
    response = avcodec_receive_packet(video_avcc, output_packet);
    if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {
      break;
    } else if (response < 0) {
      return -1;
    }

    output_packet->stream_index = index;
    output_packet->duration = enc_video_avs->time_base.den / enc_video_avs->time_base.num / dec_video_avs->avg_frame_rate.num * dec_video_avs->avg_frame_rate.den;

    av_packet_rescale_ts(output_packet, dec_video_avs->time_base, enc_video_avs->time_base);
    response = av_interleaved_write_frame(avfc, output_packet);
  }
  av_packet_unref(output_packet);
  av_packet_free(&output_packet);
  return 0;
}

Nous avons converti le flux multimédia de h264 en h265 . Comme prévu, la version du fichier multimédia h265 est plus petite que h264, tandis que le programme offre de nombreuses possibilités:

  /*
   * H264 -> H265
   * Audio -> remuxed (untouched)
   * MP4 - MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx265";
  sp.codec_priv_key = "x265-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> remuxed (untouched)
   * MP4 - MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> remuxed (untouched)
   * MP4 - fragmented MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";
  sp.muxer_opt_key = "movflags";
  sp.muxer_opt_value = "frag_keyframe+empty_moov+default_base_moof";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> AAC
   * MP4 - MPEG-TS
   */
  StreamingParams sp = {0};
  sp.copy_audio = 0;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";
  sp.audio_codec = "aac";
  sp.output_extension = ".ts";

  /* WIP :P  -> it's not playing on VLC, the final bit rate is huge
   * H264 -> VP9
   * Audio -> Vorbis
   * MP4 - WebM
   */
  //StreamingParams sp = {0};
  //sp.copy_audio = 0;
  //sp.copy_video = 0;
  //sp.video_codec = "libvpx-vp9";
  //sp.audio_codec = "libvorbis";
  //sp.output_extension = ".webm";

La main sur le cƓur, j'avoue que c'Ă©tait un peu plus compliquĂ© qu'il n'y paraissait au dĂ©but. J'ai dĂ» choisir le code source de la ligne de commande FFmpeg et tester beaucoup. J'ai probablement manquĂ© quelque chose quelque part, car j'ai dĂ» utiliser force-cfr pour h264 , et certains messages d'avertissement apparaissent toujours, par exemple, que le type de trame (5) a Ă©tĂ© changĂ© de force en type de trame (3).

Traductions sur le blog Edison:


All Articles