Fwomaj est un lecteur multimédia qui permet la sélection rapide de fichiers vidéo au retour d'une séance de tournage (on dit « dérusher ») pour un montage ultérieur. Fwomaj est sous licence GPL 2.
Sommaire
En premier lieu, il affiche la forme d'onde de la piste audio du fichier, afin de savoir tout de suite s'il s'y trouve du son, et à quel endroit.
Il permet également le transcodage de la vidéo en H264, et sa sauvegarde dans les trois containers courants: Matroska (.mkv) MPEG-4 (.mp4) et AVI (.avi) que l'utilisateur choisit à l'aide d'un menu.
Et enfin, il permet de couper le début et la fin de la vidéo à l'aide de contrôles visuels.
En bonus, il fournit également une fenêtre d'information qui liste les caractéristiques multimedia du fichier de façon exhaustive.
Code
Fwomaj est en Python 3, l'interface est en Gtk 3. Le moteur vidéo (et audio) est GStreamer 1.0.
Dépendances
Son & Lumières
C'est bien sûr FFmpeg qui fait le gros du travail : c'est lui qui génère l'image de la forme d'onde, lui encore qui découpe le fichier de temps
à temps+n
, et lui enfin qui encode tout ça, à son rythme de supertanker.
Au moment où l'utilisateur appuie sur Encode
on récupère les valeurs des menus (qui sont par défaut aux valeurs que moi, j'utilise : 30 images / secondes, Matroska) et celles des deux réglettes début/fin
, et on construit la commande FFmpeg (et le nom du fichier de destination) avec tout ça :
out_file = '{}_{}-{}_{}fps.{}'.format(filename,
str(start_time),
str(end_time),
self.frame_rate,
self.file_format)
command = [FFMPEG, '-y',
'-i', filename,
'-r', self.frame_rate,
'-strict', '-2',
'-c:v', 'libx264',
'-ss', start_time,
'-to', end_time,
out_file]
Et on envoie tout ça à une classe qui
- lance le process (en asynchrone, et rend la main) ;
- vérifie toutes les secondes qu'il n'est pas fini ;
- avertit l'utilisateur quand il est fini.
Le fichier de destination est sauvé dans le répertoire du fichier d'origine.
Au chargement d'un fichier multimedia – au lancement du programme ou après – c'est le même processus qui gère la génération de la forme d'ondes, qui s'affiche quand elle est prête (pour un gros fichier, ça peut prendre plusieurs longues secondes) sans ralentir l'interface.
Êtes-vous sûr de ne pas vouloir arrêter le processus d'annulation ?
Du coup, comme je ne lis pas la sortie de FFmpeg (je vérifie juste les codes de retour) je suis en asynchrone, yay, mais je perds la possibilité d'informer l'utilisateur de l'avancement du travail en cours, bouh ; juste un bidule qui pulse pour indiquer qu'il se passe quelque chose. Notez que FFmpeg lui-même ne le fait pas, mais il indique quelle image il est en train de traiter, or moi, j'ai plein de moyens de savoir combien d'images fait mon fichier, à commencer par Mediainfo. Eh. Dommage.
Pour arrêter un job FFmpeg qui prendrait trop de temps, il faut quitter le programme. Je ne suis pas en train de décrire un choix d'implémentation là, hein, mais la conséquence du choix de ne rien implémenter du tout pour le moment.
Chez moi, ça marche (tm) :) mais je vais quand même, c'est sûr, à un moment, faire une entrée de menu avec un raccourci clavier pour envoyer q
au process FFmpeg en cours (oui, il écoute stdin – à la fois des pipes nommés pour le streaming ET le clavier pour les commandes – et s'exprime exclusivement sur stderr, il est spé' FFmpeg) j’attends de voir un peu comment ça se passe au niveau ergonomique.
Genre de style d'UI
Afficher une image, l'étirer sur toute la largeur de la fenêtre principale, puis la re-dimensionner quand change la taille de cette dernière, est un processus assez lourd, qui implique une gestion relativement fine des événements, si on veut faire ça en utilisant les classes et méthodes de Gtk lui-même (Gtk.image
, Gtk.Pixbuf
, Gtk.DrawingArea
, Gtk.ScrolledWindow
etc.) c'est ainsi que marchait la première version en Python2/Gtk2, mais le principe est le même.
Mais Gtk3 intègre CSS3, ce qui a permis de remplacer ce genre de machin :
def on_resize(self, widget, event, window):
allocation = widget.get_allocation()
if self.temp_height != allocation.height or self.temp_width != allocation.width:
self.temp_height = allocation.height
self.temp_width = allocation.width
pixbuf = self.waveform_image.get_pixbuf().scale_simple(allocation.width,
allocation.height,
gtk.gdk.INTERP_NEAREST)
widget.set_from_pixbuf(pixbuf)
Oui oui, c'est bien ça, à chaque événement (cette fonction est un callback sur l'événement on_resize
de la fenêtre, donc des centaines de fois par seconde, dans la joie) on re-crée un pixbuf en attrapant celui de l'image en place, on le retaille aux dimensions XY
de la fenêtre, et on le remet dans l'image, qu'on remet dans le widget, ouf :/ c'est comme ça qu'on fait, au centre hippique PyGtk.
Par quelque chose comme ça:
CSS = '#WBox {background-size:100% 100%;background-image: url("{}");}'.format(uri)
style_provider.load_from_data(CSS)
Quand j'ai percuté (je suis développeur web, moi, à la base) genre sous la douche, je me suis dit c'est pas possible, ça peut pas être si simple, ça a marché au premier essai.
Mediainfo
…Est le meilleur outil d'analyse de fichiers multimédia, oui du monde, même sans les mains, point final. J'avais commencé à implémenter une usine à mazout en utilisant les fonctions de "découverte" de GStreamer, pour éviter une dépendance, mais sa souplesse et son intelligence relative étaient ridicules face aux capacités d'introspection de Mediainfo.
Bugs
Quoi ? Naaan :) Si. On commence par les petits.
Blanker (Gtk)
Quand on passe en plein écran, les contrôles disparaissent, ainsi qu'il est d'usage. Ce n'est pas du vrai plein écran d'ailleurs, au sens où on l'entend : Le gestionnaire de fenêtre n'est pas averti, pour commencer, enfin je crois, je maximise juste la Gtk.DrawingArea
où j'affiche les pixels qu'envoie le pipeline
de GStreamer.
Quand on bouge la souris, ces contrôles réapparaissent, toujours selon les protocoles en vigueur. Mais je n'ai pas réussi (j'y ai passé toute une soirée !) à démarrer un timer à ce moment, pour qu'au bout de, mettons 10 secondes d'inactivité de la souris ou du clavier, lesdits contrôles disparaissent à nouveau. Je parie que c'est une tarte à la crème, un classique du développement d'interfaces graphiques, et que quelqu'un ici va bien nous trouver une solution :)
Rendu (GStreamer)
Ça n'a bien sur pas d'incidence sur le fichier produit, mais le rendu vidéo est bordé d'un pixel de couleur, variable, comme une sorte de somme spectrale de l'image en cours, et ça bouzille le noir qui est d'usage autour de tout film décent. Mon implémentation est bancale, clairement. Je déteste ce bug minuscule et en même temps énorme, il me donne l'impression de ne rien contrôler du tout.
C'est évidemment un bout de code que j'ai copié collé de je ne sais plus d'où, car c'est pas du tout comme s'il existait ne serait-ce qu'une page dans tous les interfubles qui dit "bon, voilà, GStreamer 1.0 est sorti, même gst-launch a été renommé gst-launch-1.0 pour qu'on comprenne bien que plus rien n'est compatible, et donc voilà quelques exemples d'usage en Python3 / Gtk3 oh rien du tout, juste une implémentation de base.
On peut faire plein de choses avec GStreamer. C'est un langage de pipes vidéo assez fascinant, qu'on prototype à l'aide de l'utilitaire gst-launch, pour construire des "tuyaux" d'images qui bougent:
gst-launch-1.0 videotestsrc pattern=1 ! video/x-raw,format=AYUV,framerate=\(fraction\)10/1,width=100,height=100 ! videobox border-alpha=0 top=-70 bottom=-70 right=-220 ! videomixer name=mix sink_0::alpha=0.7 sink_1::alpha=0.5 ! videoconvert ! xvimagesink videotestsrc ! video/x-raw,format=AYUV,framerate=\(fraction\)5/1,width=320,height=240 ! mix.
Mais GStreamer est si complexe que sa documentation plane dans les limbes…
Du coup GStreamer est… magique :
Sur cette image prise en plein écran (la sublime ouverture de « Michael Clayton », T. Gilroy 2007) on remarque deux choses : l’agaçante bande de couleur évoquée plus haut (deux pixels mauves à gauches, et une sorte de frange verte intermittente d'un pixel tout autour) et les sous-titres… attend, quoi ?
Sous-titres (anti-bug, GStreamer)
Oui. GStreamer, sans que j'ai fait quoi que ce soit pour ça, m'extrait avec force diligence les sous-titres du container du film en question (Matroska, en l'occurence, qui est réputé pour ça entre autres) et me les affiche, synchronisés, en Times, c'est trop chou, mais, heu, comment je contrôle ça ? Une fonction (et pas n'importe quoi, la gestion des sous-titres je voulais même pas seulement y penser) qui tombe en marche par accident, c'est un bug aussi, non ? GStreamer 0.10 ne faisait pas ça ; on va pas se plaindre trop fort non plus, allez.
États (non-bug, Python)
Mon usage de Fwomaj est le suivant : Je double-clique sur un fichier vidéo, je le regarde ou je l'édite, bref je fais ce que j'ai à faire avec, et je le ferme. Et ça, ça marche très bien. Mais d'autres peuvent vouloir l'utiliser différemment. C'est dans cet esprit que j'ai codé un « Open File » dont je me sers jamais, par exemple ; quand on a fini d'encoder une vidéo, l'état de Fwomaj n'est pas vraiment net-net. Si ça se trouve, ça marche très bien, mais je n'ai pas testé cet usage (ouvrir un fichier, et l'encoder plein de fois avec plein de paramètres différents, puis en ouvrir un autre, dans la même instance, tiens, rien que le fait d'écraser le fichier — c'est ce qu'on fait maintenant : mêmes paramètres, même début, même fin = même nom de fichier, on écrase — c'est bien gentil, ça marche, mais si j'étais responsable Debian (par exemple) je ne laisserais clairement pas passer Fwomaj dans les dépôts officiels, à cause de ce genre de détail.
Ça vient de ma façon de coder, procédurale sinon déclarative (on dit aussi « travail de goret » par chez moi) et c'est vrai que je voulais juste un lecteur pour visionner, couper mes rushes et en harmoniser les caractéristiques pour pouvoir les monter sans devenir encore plus dingue (intermède : si tu achètes un SONY Alpha Nex5 au Japon ou aux USA, il fait des vidéos en 30 et 60 images/secondes. Si tu as la mauvaise idée de l'acheter en Europe, c'est une autre machine, modifiée en dur pour produire des vidéos en 25/50 images/secondes, et je ne ferai pas d'autre commentaire là-dessus, c'est mieux pour la tenue de cette dépêche) et que ce lecteur, je l'ai, super.
Maintenant, si j'avais un vœu pour ce petit bout de logiciel au-delà de ce qu'il m'apporte maintenant, c'est qu'un de ces gourous qui traînent par ici (nan mais vous pouvez filer en douce, trop tard hein, on lit vos commentaires, vous croyez quoi ?) se penche sur son berceau pour en ré-usiner (tiens, ça claque encore mieux en français dis-donc) le code en profondeur, oui. Fwomaj 0.3, en l'état, est un prototype. Qui marche pas mal, mais un prototype quand même. Il me semble que c'était ça l'idée de Python, non, de bricoler vite fait des trucs qui marchottent pas mal ? Bon, ben c'est réussi, merci les gars :)
Installation
En pratique, je n'ai testé Fwomaj 0.3 que sous Ubuntu 14.04 et 16.04, avec les environnements de bureau Unity et XFCE.
Debian
wget https://bitbucket.org/yphil/fwomaj/downloads/fwomaj_0.3_all.deb && sudo gdebi fwomaj_0.3_all.deb
Oui, je n'ai pas trouvé comment installer les dépendances d'un paquet (duh) avec dpgk
..?
Ou on peut aussi construire soi-même le paquet. Ne pas se laisser abuser par le répertoire ./debian
, Fwomaj est juste un script Python de 800 lignes et une icône.
git clone https://bitbucket.org/yphil/fwomaj.git
cd fwomaj
./build_package.sh
sudo gdebi -i ../fwomaj_0.3_all.deb
Et les autres systèmes?
Pour les autres Linux, ça doit être assez trivial. À minima, le script peut être lancé tel quel sur une machine où sont installées les quatre dépendances, il n'aura juste pas d'icône. Les outils qui servent à créer des paquets pour d'autres distributions n'auront probablement aucun mal à se débrouiller avec les éléments présents :
- exécutable ;
- page de man (minimale, exigée par Lintian) ;
- icônes ;
- journal (changelog) ;
- et éventuellement les fichiers de contrôle/description, tout ça n'est guère spécifique.
Pour les autre systèmes, honnêtement, je ne sais pas. Bien sûr, on doit pouvoir faire tourner Gtk3 sous Windows, FFmpeg aussi mais GStreamer, je suis moins sûr.
En tout cas, les spécificités du système d'exploitation sont abstraites autant que faire se peut :
unique_file = os.path.join(tempfile.gettempdir(), 'fwomaj-{}.png'.format(time.time()))
Devrait marcher partout, en principe.
Il parait qu'il y a Python sous AmigaOS, maintenant :|
J'utilise Fwomaj depuis une petite quinzaine comme lecteur principal, j'ai préparé une demi-douzaine d'épisodes avec, et encodé un grand nombre de fichiers d'origines diverses, ça marche bien.
Allez-y, testez ;)
Aller plus loin
- Fwomaj (507 clics)
- Téléchargements (166 clics)
# Commentaires rapides
Posté par Cyril Brulebois (site Web personnel) . Évalué à 7.
dpkg
gère les installations/mises à jour/suppressions de paquets, c'est dansapt
(& friends/concurrents) que tu trouveras la partie résolution de dépendances.La page de manuel est construite (à partir du fichier d'exemple SGML) mais n'est pas embarquée dans le paquet résultant (tu pourrais la lister dans
debian/manpages
pour qu'elle le soit, mais il faudrait probablement commencer par modifier le boilerplate).J'ai bien fait de lancer la compilation à coup de
debuild
directement, le scriptbuild_package.sh
semblant vouloir lancer un upload…En tout cas, cela semble se lancer et tourner correctement sur Debian Jessie. Peut-être que « Tous les fichiers » ou « Tous les fichiers vidéo » serait un meilleur choix dans le sélecteur de fichier ?
Debian Consultant @ DEBAMAX
[^] # Re: Commentaires rapides
Posté par yPhil (site Web personnel) . Évalué à 1. Dernière modification le 15/07/16 à 09:46.
Oui, et justement, gdebi (ça commence à faire beaucoup de solutions pour installer des machins, more is less) que je ne connaissais pas. Au passage, ça fait des années que je crois installer les dépendances quand je
dpkg -i
(heureusement, c'est bien sur très rare, et ça n'arrivera plus: qui veut installer un paquet sans ses dépendances?) ce qui n'est pas sympa, grmph.Ah tiens, je me disais aussi, que
man fwomaj
ne marchait pas (mais qui va taper ça ? :p) et de fait tu as raison, je ne fais que la générer:Mais je ne la copie pas dans l'archive! Bien vu.
Oui (seulement si le build a réussi, note bien) mais bon, à ce stade si tu n'est pas moi, tu n'as qu'à
ctrl+c
puisque tu n'as pas ma clef, et tu as le paquet. C'est moins un script qu'un aide-mémoire,build_package.sh
.Hm. C'est moins trivial qu'il n'y parait ; Actuellement, pour la requête de fichiers je construis "à l'équestre" 4 filtres, les types MIME que Fwomaj écrit, plus "Tous les fichiers", "*". Et oui, tu as raison, ce dernier devrait plutôt être un combo des premiers. Comme je l'ai dit dans la dépêche, ça ne correspond pas à mon usage, mais bon, j'ai pas non plus l'intention de sortir cette excuse tout le temps hein :) Note que les mimetypes sont correctement indiqués dans le fichier .desktop (j'ai recopié celui de smplayer) et que Fwomaj
se comportedoit se comporter convenablement pour clic-droit "ouvrir avec…" avec un WM décent.Sans ça, pendant la rédaction de la dépêche, palm123< m'a donné plein de liens précieux pour faire une image docker (et pour comprendre exactement ce que c'est que Docker tout court pour commencer) heureusement que j'ai cliqué sur la plupart (mais pas tous) car le log est dans devnull maintenant..? :/
[^] # Re: Commentaires rapides
Posté par yPhil (site Web personnel) . Évalué à 2. Dernière modification le 15/07/16 à 10:27.
La meilleure implémentation serait celle qui interroge à la fois FFmpeg et GStreamer (c'est très simple, il y a une commande pour ça, en tout cas il y a
ffmpeg -codecs
) pour savoir exactement quels types de fichier on peut lire et écrire sur cette very machine. Quand je dis que ce n'est qu'un proto, c'est exactement ça que je veux dire.Allons plus loin, car on est au coeur du sujet, là. Toutes les combinaisons codec/conteneur ne sont pas possibles. Enfin si, tout est possible, mais certaines combinaisons bizzares ne seront pas faisables en une commande FFmpeg. Il en faudra plusieurs.
Tout ça
devraitpourrait être géré par une logiqueMais pour ça il faut un spécialiste des invocations ésotériques de FFmpeg. Je dis ésotérique mais c'est pas très juste, c'est simplement très puissant, mais à la différence de GStreamer, c'est bien documenté. Juste un exemple, le switch
-r $Nombre_images/secondes
selon que tu l'utilises en entrée (CàD que tu le spécifies juste après le premier-i
) fait les choses très différemment:Or selon que tu utilises telle option ou telle autre, tu pourras utiliser tel codec (note que le codec de Fwomaj est en dur: Dommage) or nous, comme on ne lit basiquement pas les erreurs de FFmpeg, j'ai choisi de limiter les paramètres de ses invocations à ceux que je connais et utilise et qui sont compatibles entre eux, ce qui fait peu ; note que bon, ça correspond aux standards pour autant que je sache.
Tu vois la complexité grandissante si tu veux faire quelque chose de réellement intelligent et "universel" (disons terrestre, déjà) ? Overwhelming. On peut juste s'approcher au plus près de quelque chose d'utilisable et standard.
[^] # Commentaire supprimé
Posté par Anonyme . Évalué à -7. Dernière modification le 15/07/16 à 12:58.
Ce commentaire a été supprimé par l’équipe de modération.
[^] # Re: Commentaires rapides
Posté par yPhil (site Web personnel) . Évalué à 3.
Yep. Il faudrait des années pour tout tester. Avoir plein de fichiers bien pourris, avec des corruptions dans des endroits bien tordus… Faire plein de combinaisons improbables, ou pas.
Note que ça ressemble au job idéal pour "super test unitaire". "Est-ce un oiseau, un avion ? Non, c'est super unit-test" :) Il faudrait commencer par coder une routine qui corrompt exprès des fichiers multimedia pour avoir du matos pour tester.
Donc oui, considérant l'alternative, tavu, mon approche "juste les valeurs qui marchent en dur" a du sens :) Bon par ailleurs oui, il semble y avoir beaucoup de NIH sur les codecs et les containers, mais aussi plein d'autres critères.
Voir les journaux et comms de Zenitram sur le sujet. Bonne chance pour les retrouver dans la masse ;)
Spécifiquement, j'aimerais bien (le mot est faible) un journal ou une dépêche sur les horreurs qu'il a forcément du rencontrer dans sa tache pharaonique de "je lis tous les fichiers multimedia" plus on bosse avec (et bon, j'ai fait Louis Lumière dans les 80s, en fait les formats image/videos, j'ai passé un peu de temps sur la question quand même, sans parler de mes multiples - et tout nazes à coté - parsers à moi) plus on réalise l'ampleur de la tache. Par exemple quand je pense à pourrir soi-même les fichiers (accents chelous dans les tags, je sais pas, anything) pour écrire des tests unitaires, j'ai bon, non ?
Je veux dire, à chaque itération du code de mediainfo, il faut tout re-tester, non ? Et les évolutions des codecs et des containers, des specs ? Ze-ni-tram, un-jour-nal! (tous avec moi) ZE-NI-TRAM, UN-JOUR-NAL! :D
[^] # Commentaire supprimé
Posté par Anonyme . Évalué à -7. Dernière modification le 15/07/16 à 15:59.
Ce commentaire a été supprimé par l’équipe de modération.
[^] # Re: Commentaires rapides
Posté par Zenitram (site Web personnel) . Évalué à 1.
Mouais, mais pour décrire toutes les merdes et les hacks de MI pour le gérer, il me faudrait 1 ou 2 semaines (ou plus) pour écrire le journal. Et c'est pas un taf des plus motivant.
[^] # Re: Commentaires rapides
Posté par yPhil (site Web personnel) . Évalué à 2.
Je vois très bien ce que tu veux dire. C'est (très) dur à écrire, ce genre de trucs, mais c'est très rigolo à lire :p
# Génial
Posté par Alex G. . Évalué à 5.
J'ai adoré ta dépêche, c'est vraiment en plein dans l'idée que je me fait de linuxfr : c'est une présentation mais aussi un retour d'expérience et surtout un partage.
# MediaInfo
Posté par Zenitram (site Web personnel) . Évalué à 2.
J'aime.
(désolé)
Par curiosité, quel reproche fais-tu sur la souplesse de GStreamer? A première vue, son API semble propre.
Et tu as rencontré des fichiers qui t'ont donné du fil à retordre ("intelligence relative")?
[^] # Re: MediaInfo
Posté par yPhil (site Web personnel) . Évalué à 2.
?
Oui. Concrètement, des fichiers dont mon parser n'arrivait pas à lire la durée et le nombre de frames (les infos qui m'intéressent en priorité, ah j'ai oublié de dire dans le journal que MI, du coup, ne sert pas qu'à remplir la fenêtre "file info" mais également à donner les infos de base sur le fichier à l'ouverture, pour construire les réglettes, entre autres.
Donc oui, quand à un moment je charge un fichier, et que mon début d'implémentation (bien sur que c'est pas la faute de GStreamer) me renvoie une valeur différente que celle que me donne MI, je réfléchis pas, je benne le premier et garde le second.
# Improvements ?
Posté par Ezka . Évalué à 5.
Salut,
j'ai rapidement jeter un œil a ton code (le nom m'a fait marrer): bon on est d'accord pour une PoC c'est bien mais ça risque d'en dissuader plus d'un =D.
Mes modestes conseils (qui son mes guidelines, vous aurez surement les votres) qui pourraient t'aider:
essaie de séparer la partie "lib" de la partie interface GTK et de la partie exécution de script pour commencer. Simplement tu peux faire un package fwomaj/ dans lequel tu crées 3 modules par exemple libfwomaj.py, cmd_scripts.py, gtk_ui.py ; En gros ça te permet si un jour tu veux faire un support en CLI d'ajouter un cli_ui.py qui utilisent tes popen de libfowmaj.py sans tout refaire.
pour mediainfo tu peux utiliser le wrapper python PyMediainfo https://pymediainfo.readthedocs.io/en/latest/ ça t'évite d'aller ouvrir un popen pour récupérer les infos ;)
D'une façon générale, j'aurai fait un objet Fwomaj qui lui va réaliser le binding entre la video et ton interface. Par exemple je trouverai sympa d'avoir un truc du style:
La classe Fwomaj te permettant de récupérer les infos via mediainfo, d'initialiser ton pipeline GStreamer, de faire ta waveline le tout sans être dépendant de GTK. Par exemple ça peut donner:
A vérifier mais ce genre de truc te permet de virer le fichier png de ta waveline quand tu delete l'objet par exemple. Au niveau des UI faire le nettoyage c'est souvent le bordel, même si au départ je n'ai qu'une ligne, je fais une fonction ui_cleanup(…) en prévision du futur (NB: tu peux aussi t'en servir à l'init pour être sur d'avoir quelque chose de cohérent entre les états/variables/éléments de ton UI).
Ou encore ta classe WForm avec cette tête:
C'est simple à comprendre, ça t'évite d'aller mettre en global ta commande, même si tu caches un peu la misère dans une classe =D.
Bref mes 2 copecs si tu as envie d'y re-travailler dessus pour l'améliorer. =)
[^] # Re: Improvements ?
Posté par yPhil (site Web personnel) . Évalué à 1.
Bien vu, ce serait effectivement plus élégant comme ça. Mais là j'ai trop de prod en retard pour retourner dans le code, donc je tourne autour des bugs ;p
Suivre le flux des commentaires
Note : les commentaires appartiennent à ceux qui les ont postés. Nous n’en sommes pas responsables.