par , 03/06/2020 à 18h23 (4542 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 :
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 :
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 :
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 :
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 :
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 !!