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

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 Lesparties 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 \
-c: v libvpx-vp9 -c: libvorbis \
bunny_1080p_60fps_vp9.webm
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 simplementffmpeg -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 \
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:
$ 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
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:a libvorbis -b:a 128k -vn -f webm -dash 1 audio_128k.webm
$ 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:
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);
}
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");
fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255);
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Ă©onsmaintenant 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];
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);
packet.pos = -1;
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
Duration: 00:00:10.03, start: 0.000000, bitrate: 2751 kb/s
Program 1
Metadata:
service_name : Service01
service_provider: FFmpeg
Stream
Stream
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.
avfc = avformat_alloc_context();
avformat_open_input(avfc, in_filename, NULL, NULL);
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";
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);
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];
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;
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);
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:
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";
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";
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";
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";
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: