Voir le flux RSS

ericb2

[Actualité] Produire ses vidéos (audio + vidéo synchronisés) sous Linux, pour le prix de la webcam

Noter ce billet
par , 03/06/2020 à 18h23 (1646 Affichages)
Pour ceux que cela intéresse, j'ai repris un projet (on dit "forké") et j'ai ajouté ce dont j'avais besoin pour créer ffmpeg-cpp2 sous Linux.

Il s'agit d'une petite API en C++ qui encapsule les bibliothèques qui constituent ffmpeg (libavformat, libavutil, libavdevice, libavcodec, ...)

L'utilisation basique de la webcam est donnée dans la petite démo qui s'appelle remux_webcam (voir ci-dessous). Mais les filtres, dont je ne parle pas ici, c'est encore plus génial !


L'API est basée sur la notion de flux entrée/sortie :
  • source audio : raw (par exemple venant de la webcam), ou frame (bande son venant d'une vidéo donnée ;
  • source vidéo (un fichier, un flux réseau, un flux raw (venant d'une webcam)
  • l'extraction du son ou de l'image ;
  • le filtrage (plus de 100 filtres, issus de ffmpeg : rotation, zoom, crop etc) ;
  • le démultiplexage : on extrait le son ou l'image d'un flux (le décodage est automatique) ;
  • on peut ensuite mélanger les flux que l'on a extrait avec un multiplexeur.


En gros, on peut faire ce qu'on veut, quelle que soit la source, et SANS être obligé d'utiliser l'interface graphique du développeur. Comme ça, vous faîtes ce que vous voulez.

Exemple : j'ai associé la bande son de Hells Bells (AC-DC) au début de la vidéo BigBuckBunny. ça rend pas mal du tout :-)

Le dépôt (framagit) est là : ffmpeg-cpp2 sur framagit

Afin de protéger mon travail, j'ai mis la licence GPL V3, mais je pense repasser un jour sous LGPL.

Dans les démos, on a plein d'exemples qui devraient fonctionner tout droit.

Voir : démos

Application à la webcam :

  • j'utilise alsa + v4l2 sous Linux (sous Windows, ça pourrait être dshow à la place de v4l2, et DirectSound à la place d'alsa)
  • le démultiplexeur pour la webcam (partie vidéo) est le même que pour les sources type fichier, mais j'ai surchargé le constructeur, pour des raisons d'utilisabilité
  • pour le son, j'ai implémenté une nouvelle classe, dérivant d'une source classique


Les problèmes rencontrés : la synchronisation, et la bande son qui ne durait quasiment pas. Le problème venait du fait qu'il fallait deux fils d'exécution séparés pour le son et la vidéo sur la même webcam. Sinon alsa (avec pulse ou hw:1,0 etc) fonctionne super bien (j'utilise Linuxmint)

Les dépendances :

  • ffmpeg (toutes les bibliothèques associées en fait. J'utilise la version 4.2.2
  • la libpthread (pour l'enregistrement séparé son / image synchrone)
  • certaines bibliothèques (mais on peut s'en passer) de codecs (h264, liblamemp3, libvpx-vp9, etc
  • v4l2 , qui est l'API standard pour Linux
  • la libasound (bibliothèque alsasound)
  • la bibliothèque c++ standard : std::chrono (pour l'instant, on enregistre une durée donnée, mais quand il y aura un bouton start/stop, la durée ne sera plus un problème)
  • std::fstream pour la suppression des fichiers audio et vidéo intermédiaires


TODO : implémenter VAAPI pour le décodage et l'encodage


Le contenu du programme est trivial, mais pour une vraie application, j'aurais créé des nouvelles classes (TODO). Les commentaires sont dans le code.


Initialisation :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
 * File remux_webcam.cpp
 * Copyright Eric Bachard / 2020 05 08
 * This document is under GPL v3 license
 * see : http://www.gnu.org/licenses/gpl-3.0.html
 */

#include <iostream>
#include <chrono>
#include <ffmpegcpp.h>
#include <thread>         // std::thread
#include <fstream>        // std::remove

static bool bRecording = false;

using namespace ffmpegcpp;
using std::string;
using std::cerr;

//#ifdef ALSA_BUFFER_SIZE_MAX
#undef ALSA_BUFFER_SIZE_MAX
#define ALSA_BUFFER_SIZE_MAX  524288

// les fichiers temporaires contenant le son et les images
const char * audio_file = "../videos/audio.mp4";  // aac (s32le ?)
const char * video_file = "../videos/video_H264.mp4";  // h264

Enregistrement Audio :


Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void record_Audio()
{
    const char * audioDevice = "pulse";  // fonctionne avec pulseaudio ici
    //const char * audioDevice = "hw:1,0";
    const char * audioDeviceFormat = "alsa";

    // on crée une instance du container audio
 
   Muxer* Amuxer  = new Muxer(audio_file);   

    // paramètres usuels
    int audioSampleRate = 44100;
    int audioChannels   = 2;


    // on choisit d'encoder le son avec le codec aac, avec une instance de l'encodeur qui suit 
    AudioCodec         *   audioCodec = new AudioCodec(AV_CODEC_ID_AAC);
    AudioEncoder       *   audioEncoder = new AudioEncoder(audioCodec, Amuxer);


    // définition de la source audio (voir le constructeur pour retrouver le mécanisme
    // habituel de ffmpeg
    RawAudioFileSource *   audioFile = new RawAudioFileSource( audioDevice,
                                                            audioDeviceFormat,
                                                            audioSampleRate,
                                                            audioChannels,
                                                            audioEncoder);

    // préparation
    audioFile->PreparePipeline();

    // bRecording est une variable globale, car on doit pouvoir terminer le fil d'exécution
    while (!audioFile->IsDone())
    {
        audioFile->Step();

        if (bRecording == false)
            audioFile->Stop();
    }

    Amuxer->Close();

    if (audioEncoder != nullptr)
        delete audioEncoder;

    delete Amuxer;
}

Même chose pour la vidéo :


Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void record_Video()
{
    int width  = 1280; // 1920;
    int height = 720;  // 1080;
    int fps = 24;  // Logitech prefered fps value

    AVRational frameRate = { 24, 1 };

    // These are example video and audio sources used below.
    const char * videoDevice = "/dev/video0";
    AVPixelFormat outputPixFormat= AV_PIX_FMT_NV12;

    Muxer* Vmuxer = new Muxer(video_file);
    H264Codec  * vcodec = new H264Codec();

    VideoEncoder * videoEncoder = new VideoEncoder(vcodec, Vmuxer, frameRate, outputPixFormat);

    Demuxer * demuxer  = new Demuxer(videoDevice, width, height, fps);
    demuxer->DecodeBestVideoStream(videoEncoder);
    demuxer->PreparePipeline();

    while (!demuxer->IsDone())
    {
        demuxer->Step();

        if (bRecording == false)
        {
            demuxer->Stop();
        }
    }

    // close the first muxers and save separately audio and video files to disk
    Vmuxer->Close();

    if (videoEncoder != nullptr)
        delete videoEncoder;

    delete Vmuxer;
}


Création et assemblage de la vidéo finale :


Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
void create_final_Video()
{
    const char * final_file = "../videos/final_video.mp4";  // h264 + aac (or vp9 + aac)

    Muxer* AVmuxer = new Muxer(final_file);

    AudioCodec * faudioCodec = new AudioCodec(AV_CODEC_ID_AAC);
    H264Codec  * fvcodec = new H264Codec();

    try
    {
        // Create encoders for both
        VideoEncoder* fvideoEncoder = new VideoEncoder(fvcodec, AVmuxer);
        AudioEncoder* faudioEncoder = new AudioEncoder(faudioCodec, AVmuxer);

        // Load both audio and video from a container
        Demuxer* videoContainer = new Demuxer(video_file);
        Demuxer* audioContainer = new Demuxer(audio_file);

        // Tie the best stream from each container to the output
        videoContainer->DecodeBestVideoStream(fvideoEncoder);
        audioContainer->DecodeBestAudioStream(faudioEncoder);

        // Prepare the pipeline. We want to call this before the rest of the loop
        // to ensure that the muxer will be fully ready to receive data from
        // multiple sources.
        videoContainer->PreparePipeline();
        audioContainer->PreparePipeline();

        // Pump the audio and video fully through.
        // To avoid big buffers, we interleave these calls so that the container
        // can be written to disk efficiently.
        while ( (!videoContainer->IsDone()) || (!audioContainer->IsDone()))
        {
            if (!videoContainer->IsDone())
                videoContainer->Step();

            if (!audioContainer->IsDone())
                audioContainer->Step();
        }

        // Save everything to disk by closing the muxer.
        AVmuxer->Close();
    }
    catch (FFmpegException e)
    {
        cerr << e.what() << "\n";
        throw e;
    }

    delete AVmuxer;
}


Et enfin, le programme principal :


Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
int main(void)
{
    // obligatoire sinon la webcam n'est pas intialisée correctement
    avdevice_register_all();
    //avformat_network_init(); // future use

   // globale, permet de terminer proprement les 2 fils audio et vidéo
    bRecording = true;

    // on démarre l'enregistrement, en appelant séparément les deux 
   // process qui seront de fait quasi-synchronisés 
    std::thread first (record_Audio);
    std::thread second (record_Video);

    // on va enregistrer 1 min = 60 secondes
    auto start = std::chrono::steady_clock::now();
    auto current_time = std::chrono::steady_clock::now();
         std::chrono::duration<double> elapsed_seconds = current_time - start;
    do
    {
        current_time = std::chrono::steady_clock::now();
        elapsed_seconds = current_time - start;

    } while ((elapsed_seconds.count()) < (60));


    // on annonce aux fils d'exécution qu'il faut terminer
    bRecording = false;

    // on recolle les morceaux, ce qui donne le temps de finaliser les
   // 2 fichiers temporaires, et de ne pas utiliser des fichiers en cours d'écriture

    first.join();
    second.join();


    // assemblage des 2 fichiers temporaires (audio / vidéo)
    create_final_Video();


    // c'est fini !!
    std::cout << "Encoding complete!" << "\n";


   // il peut être intéressant de conserver les fichiers temporaires
#define TEST
#ifdef TEST
    std::remove(audio_file);
    std::remove(video_file);

     bool failed = (std::ifstream(audio_file) || std::ifstream(video_file));

    if(failed)
    {
        std::perror("Error opening deleted file");
        return 1;
    }
#endif

    return 0;
}

Toute retour d'expérience et/ou aide est bienvenu, évidemment !!

Envoyer le billet « Produire ses vidéos (audio + vidéo synchronisés) sous Linux, pour le prix de la webcam » dans le blog Viadeo Envoyer le billet « Produire ses vidéos (audio + vidéo synchronisés) sous Linux, pour le prix de la webcam » dans le blog Twitter Envoyer le billet « Produire ses vidéos (audio + vidéo synchronisés) sous Linux, pour le prix de la webcam » dans le blog Google Envoyer le billet « Produire ses vidéos (audio + vidéo synchronisés) sous Linux, pour le prix de la webcam » dans le blog Facebook Envoyer le billet « Produire ses vidéos (audio + vidéo synchronisés) sous Linux, pour le prix de la webcam » dans le blog Digg Envoyer le billet « Produire ses vidéos (audio + vidéo synchronisés) sous Linux, pour le prix de la webcam » dans le blog Delicious Envoyer le billet « Produire ses vidéos (audio + vidéo synchronisés) sous Linux, pour le prix de la webcam » dans le blog MySpace Envoyer le billet « Produire ses vidéos (audio + vidéo synchronisés) sous Linux, pour le prix de la webcam » dans le blog Yahoo

Mis à jour 05/06/2020 à 08h37 par LittleWhite (Mise en forme)

Catégories
Programmation , C , C++

Commentaires

  1. Avatar de lilivve
    • |
    • permalink
    Bonjour Eric,

    [Comme je te le disais ailleurs] j'ai trouvé difficile il y a quelques temps d'utiliser ffmpeg dans un projet en C++.

    Ma difficulté tenait à deux choses :
    - Utiliser une API en C, n'étant pas familier de la programmation en C. Même si c'est loin d'être insurmontable si on est habitué au C++, c'était quand même un peu déroutant.
    - Le manque de documentation concernant ffmpeg. J'ai peut-être raté quelque chose, mais je garde le souvenir d'avoir surtout trouvé des exemples (pas tant que cela) que j'essayais d'adapter, avec le sentiment d'être un peu dans l'obscurité. J'aurais aimé trouver une vue d'ensemble du fonctionnement et de la logique de ffmpeg sans avoir besoin de plonger direct dans son code. J'ai été très étonné de ne rien trouver de mieux, ffmpeg est un formidable outil, utilisé dans pas mal de logiciels, je me suis demandé comment s'en sortait les autres développeurs.

    J'ai l'impression que si un jour j'ai nouveau besoin de faire de l'encodage vidéo dans un programme ton wrapper pourra m"aider (je dis wrapper mais j'ai l'impression que c'est un peu plus non ?), d'autant plus qu'il est fourni avec de nombreux exemples. Je t'encourage donc à continuer, et merci pour ce partage.
  2. Avatar de ericb2
    • |
    • permalink
    Citation Envoyé par lilivve
    Bonjour Eric,

    [Comme je te le disais ailleurs] j'ai trouvé difficile il y a quelques temps d'utiliser ffmpeg dans un projet en C++.

    Ma difficulté tenait à deux choses :
    - Utiliser une API en C, n'étant pas familier de la programmation en C. Même si c'est loin d'être insurmontable si on est habitué au C++, c'était quand même un peu déroutant.
    - Le manque de documentation concernant ffmpeg. J'ai peut-être raté quelque chose, mais je garde le souvenir d'avoir surtout trouvé des exemples (pas tant que cela) que j'essayais d'adapter, avec le sentiment d'être un peu dans l'obscurité. J'aurais aimé trouver une vue d'ensemble du fonctionnement et de la logique de ffmpeg sans avoir besoin de plonger direct dans son code. J'ai été très étonné de ne rien trouver de mieux, ffmpeg est un formidable outil, utilisé dans pas mal de logiciels, je me suis demandé comment s'en sortait les autres développeurs.

    J'ai l'impression que si un jour j'ai nouveau besoin de faire de l'encodage vidéo dans un programme ton wrapper pourra m"aider (je dis wrapper mais j'ai l'impression que c'est un peu plus non ?), d'autant plus qu'il est fourni avec de nombreux exemples. Je t'encourage donc à continuer, et merci pour ce partage.
    Pour être honnête, c'est surtout celui qui a créé ffmpeg-cpp qui a bien bossé. Mais la version Linux n'avançait pas (malgré les demandes) et le code n'est pas si clean (par exemple, pb de visibilité avec les contextes AVFormat, qui n'est toujours pas résolu , ce qui m'a forcé à utiliser 2 threads). Il reste aussi plein de warnings, avec des re-définitions excessives qui doivent encore causer quelques bugs.

    Mais tu as raison, ffmpeg est plus compliqué à utiliser sous forme C/C++ qu'en script. En ce qui me concerne, c'est en aidant plein de monde à corriger des warnings (par exemple : https://github.com/starkdg/phvideoca...8ac0520f86359c) et en faisant des essais que j'ai commencé à y voir plus clair. Remarque : ça fait quand même plus d'un an que je suis dedans.

    Et c'est le fait de pouvoir utiliser des "briques" pour extraire / associer du son, des images m'a décidé à l'utiliser, car quand on n'a plus à mettre les mains dans le cambouis, c'est un vrai plaisir.

    Si tu as besoin d'aide, pas de problème : attention, la plupart des démos fonctionnent bien, mais les résultats demandent quelques ajustement (en particulier MPEG2). En cas de besoin, tu ouvres une issue sur le framagit et je verrai si je peux aider. amha, le truc, c'est de bien définir ce que tu veux faire, et le reste, c'est normalement facile à faire.

    Ce qui m'impressionne le plus, ce sont les filtres :

    - rotation 90°, crop, zoom : ça fonctionne vraiment comme avec ffmpeg, mais dans le code (un vrai plaisir). Le gars qui a implémenté ça (ce n'est pas moi) a vraiment été bon.

    Pour la suite, je compte implémenter VAAPI : si j'ai bien compris, il me manque la création d'un contexte hardware, et l'accélération matérielle (Intel seulement) devrait fonctionner quasi directement, toujours avec les filtres.


    En tout cas, merci encore pour le retour et les encouragements :-)

    Edit : correction du lien
    Mis à jour 04/06/2020 à 17h03 par ericb2