Journal Python, Lies and Video Files

Posté par  . Licence CC By‑SA.
Étiquettes :
24
13
sept.
2020

Sommaire

Attention: ami lecteur, ce journal est très long, contient des termes en anglais, et les noms d'éditeurs et de logiciels propriétaires ont été traduits plus ou moins littéralement, parce que ça m'amuse.
Le titre de ce journal est inspiré d'un titre de film célèbre, et je n'affirme nullement que qui que ce soit ait menti dans l'histoire qui suit.

Je bosse dans le monde des médias, dans un secteur plutôt informatisé nommé la postproduction qui est, en très gros, tout ce qui se passe après un tournage et avant une diffusion: acquisition, montage, une partie des VFX, étalonnage vidéo, mixage audio, etc. Avec la "convergence des médias", nous fournissons potentiellement des services à tout le monde, mais, dans la pratique, ça se limite presque uniquement aux gens de la TV, les personnes de la radio ayant des besoins assez différents, et celles du Web snobant toutes les autres (humour). Nous gérons également la diffusion des sujets des émissions TV en direct lorsque cette diffusion a lieu sur notre site géographique, pour des raisons pratiques. Les moyens de diffusion depuis les cars-régies en déplacement sont fournis par d'autres équipes. Inutile de préciser que tout est à 1000000% propriétaire.

Mon domaine, c'est la postproduction audio, particulièrement—mais pas uniquement—l'enregistrement et le mixage son, que je nommerai simplement "mixage" par la suite.

Voici quelque temps, nous sommes passés de la version "A" d'un logiciel de montage vidéo propriétaire bien connu—appelons-le Boue Première—à la version "B". Il se trouve que le mixage doit pouvoir importer les fichiers AAF exportés par ce logiciel de montage, et qu'il semble y avoir eu des changements à ce niveau.

■ ⏪ ■ ▶

Nous employons un bus d'entreprise qui sert de lien entre tous les îlots technologiques—le montage vidéo et le mixage audio, par exemple. L'échange de données se fait par-dessus ce bus à l'aide de connecteurs, dont celui qui équipe mon périmètre a été développé par l'équipe d'intégration. Ce connecteur établit le lien entre le bus et notre logiciel de mixage audio désormais vieillissant, que nous appellerons Belle Lumière.

Belle Lumière a deux vilains défauts. Premièrement, il est sale: si on lui permet d'accéder à un répertoire en écriture, il va y écrire des fichiers partout, comme certains systèmes connus. Deuxièmement, il a un comportement erratique lorsqu'il n'a pas accès en écriture à tous les répertoires d'un volume désigné comme accessible en écriture. Il a donc été décidé qu'il serait limité à un partage précis du stockage centralisé de la postproduction, dans lequel tous les répertoires lui seraient accessibles en écriture. L'ennui, c'est qu'il doit pouvoir accéder aux médias qui sont situés hors de ce partage, et qu'il a donc fallu créer des liens symboliques côté serveur pour qu'il puisse accéder à ça et à rien d'autre. En très simplifié, du point de vue d'une station de montage sous Fenêtres, on a donc quelque chose comme ceci:

Z: (\\stockage\postproduction)
  |-- partage
      |
      |-- medias <..........
  |-- mixage               :
      |                    :
      |-- medias (symlink)..
      |-- projets
  |-- montage
      |
      |-- projets

Et du point de vue d'une station de mixage, également sous Fenêtres:

Z: (\\stockage\postproduction\mixage)
  |
  |-- medias (symlink)
  |-- projets

C'est là que AAF s'est montré plus ennuyeux que d'habitude.

AAF, il faut voir ça comme un historique de la postproduction: on y trouve des timelines, des clips, des transitions… Les médias1 peuvent y être imbriqués ou être des références à des fichiers externes. C'est le second cas qui est employé chez nous. Mais voilà, les chemins renseignés dans le AAF sont fort logiquement fonction de la machine qui a servi à le concevoir. Or, le montage accède au partage racine, et un média fubar.mxf aura pour lui le chemin Z:\partage\medias\fubar.mxf, alors que Belle Lumière doit y accéder en suivant le lien symbolique Z:\medias, ce qui donne pour chemin absolu Z:\medias\fubar.mxf. Pour Belle Lumière, les fichiers renseignés dans le AAF sont donc simplement introuvables.

Vous me direz, pourquoi ne pas imbriquer les médias? Simplement parce que ça prend plus longtemps lorsqu'on veut juste mettre à jour un petit bout de son: il faut alors effectuer une nouvelle restitution du son et de la vidéo, et imbriquer le tout dans le AAF2. Quand on produit un journal télévisé, ça n'est pas acceptable. La place occupée sur le stockage aurait également une fâcheuse tendance à croître.

Pour pallier ce nouveau problème, l'équipe d'intégration a écrit un autre outil, à l'aide du SDK de référence AAF, qui serait Open Source3 (le choix du vocabulaire est le leur). Malheureusement, AAF, c'est apparemment du Structured Storage auquel on accède à l'aide dudit SDK via COM. Ce n'est pas bien différent d'un fichier de la suite Microsoft Office, autrement dit une sorte de base de données d'objets sérialisés dans un fichier plat. Seuls les types des objets et leurs relations diffèrent. Ce SDK est relativement compliqué, mal documenté, a 20 ans, doit être patché pour compiler avec un compilateur moderne, et on attend toujours certains exemples d'utilisation évoqués en 2004. Sous GNU/Linux, une implémentation minimale de COM est fournie avec le SDK. C'est déjà ça.

Quoi qu'il en soit, l'équipe d'intégration a écrit un outil qui a le mérite de fonctionner, en C#—ce qui rend peut-être l'emploi de COM moins pénible—et qui modifie les chemins dans les AAF afin que Belle Lumière puisse trouver ses petits. Dans le même temps, parce que Belle Lumière ne tolère la présence que d'une seule piste vidéo dans un projet, l'outil supprime toutes les pistes vidéos du AAF, et en insère une nouvelle qui contient une référence à la vidéo issue du montage. On passe de quelque chose comme ça:

[v3][effet]                         [effet]
[v2][    réf.1     ]   [     réf.3    ][        réf.4        ]
[v1]            [ réf.2 ]

A quelque chose comme ça:

[v1][réf.1 vers la vidéo restituée exportée par Boue Première]

■ ⏩ ■ ▶

Passage à la version "B" de Boue Première, donc.

Depuis, certains des projets ouverts dans Belle Lumière sont vides. Rien n'est présent selon lui: ni timeline, ni référence vers le moindre média; comme si le fichier AAF ne contenait rien, alors que le monteur vidéo a bien exporté quelque chose. Catastrophe, mais qu'à cela ne tienne: après quelques semaines et beaucoup de grincements de dents, l'équipe d'intégration nous donne une nouvelle version de son outil, contournant le problème qui serait dû à des pistes de longueur nulle exportées par Boue Première. Désormais, le montage exporte donc un AAF qui ne contient que des références aux clips vidéos (qui sont toujours remplacées par une unique référence comme décrit ci-dessus), et un OMF qui contient des références aux sons. OMF, c'est un peu l'ancêtre de AAF, mais il se limite au son et est encore plus mal documenté. Au moins, on peut tout importer manuellement en cas de problème, ce qui n'était pas le cas avec les AAF fournis depuis la mise à jour du montage.

Fantastique, c'est donc réglé! En fait non. Contrairement à AAF, OMF ne contient pas de timecode source: il débutera donc là où vous le placerez sur votre timeline. Quand il y a du son dès le début du projet—ce qui est très fréquent—ça ne se remarque pas. Quand il n'y a pas de son au début du projet, et que le connecteur cale tout au même timecode, la vidéo et le son… ne sont plus synchrones.

Au lieu d'avoir une timeline comme ça:

[v1][           vidéo           ]
[a1]    [audio 1]       [audio 3]
[a2]            [audio 2]

On se retrouve avec ça:

[v1][           vidéo           ]
[a1][audio 1]       [audio 3]
[a2]        [audio 2]

La solution? Jusqu'à présent, aucune, si ça n'est synchroniser à la main. Pour une grosse production où plusieurs jours sont dédiés au mixage son, il suffit que l'ingénieur du son s'entretienne avec le monteur pour savoir à quel moment débute le son. Pour des productions où il faut aller très vite, comme un journal télévisé où le mixage audio d'un sujet se termine parfois 30 secondes avant son passage à l'antenne, c'est impensable.

Après une petite discussion, il nous a été expliqué que le problème viendrait de Boue Première, dont les AAF seraient "foireux" et dont on ne parviendrait pas à lire certaines métadonnées.

Sceptique et curieux de nature, j'ai commencé à me renseigner sur AAF et, après m'être arraché les cheveux, j' ai vaguement compris l'agencement employé par Boue Première, même s'il reste très nébuleux. En tout cas, je le comprends suffisamment pour tenter de répliquer le travail de l'équipe d'intégration afin de voir si je bute sur le même problème.

C'est là que je me suis rendu compte de l'absence de documentation et d'exemples du SDK de référence. En particulier, il n'existe pas vraiment d'exemple fonctionnel montrant comment créer une timeline vidéo référençant simplement un fichier externe. L'intégrer au AAF, oui. Le référencer, non. L'exemple le plus proche concerne le référencement d'un fichier wav. Je me suis donc dit que l'intégration était partie de ça, mais pour référencer des fichiers de la sorte, il faut les greffons du SDK, qui sont fournis avec le reste du code, mais qui sont absents de l'installation de l'outil développé en interne. Sans ces greffons, et sans les avoir initialisés, il n'est pas possible de référencer ou d'intégrer des médias: on obtient systématiquement une erreur invalid codec. Ce n'est donc pas la méthode employée par nos intégrateurs.

En fouillant un peu, je me rends compte que notre outil crée des fichiers XML qui correspondent aux fichiers AAF. Le SDK fournit justement un outil de transformation vers et depuis XML. J'imagine donc que l'intégration a recours à l'astuce suivante: convertir le AAF en XML, transformer le XML, convertir le résultat en AAF. Elle me soutient que ça n'est pas le cas, mais je soupçonne que le problème de projets vides provienne d'une telle étape, à moins qu'ils n'attaquent directement le Structured Storage, ce que C# rend probablement assez facile… et dangereux. Je ne pourrai en tout cas affirmer que cette transformation est problématique que si je parviens moi-même à traiter les AAF sans rencontrer les mêmes difficultés.

Reste que l'absence de documentation n'aide en rien: même quand je finis par comprendre comment ajouter cette piste vidéo, je ne trouve aucune documentation sur les AUID et les typedef des conteneurs et algorithmes de compression supposés être employés pour décrire les fichiers vidéo que nous produisons. La verbosité et le typage faible de COM me rendent très peu productif. Le temps passe…

Après quelques jours avec ce SDK, j'ai une meilleure compréhension de AAF, je peux modifier les chemins vers les médias, supprimer les pistes vidéo, en ajouter une, mais toujours pas référencer de fichier vidéo. Je me tourne alors vers un projet nettement moins frustrant, à savoir OpenTimelineIO ("OTIO"), mené par Pixar et particulièrement prometteur… si on n'a pas un grand besoin de AAF. En effet, lors de mes essais, le plugin AAF de OTIO ne permettait pas encore de gérer les transitions, et mes tentatives pour modifier les chemins vers des médias externes se soldaient systématiquement par des MissingReference en lieu et place des NetworkLocator que j'espérais trouver. Pixar n'utilise pas de live action, les AAF sont donc sans doute employés uniquement pour le Sweat Box, qui peut franchement se passer de transitions, on imagine du coup facilement qu'ils ne fassent pas de AAF la priorité (mais le projet est actif).

Dépité, je me suis tourné à contrecœur vers Java et MAJ, développé par l'AMWA tout comme le SDK AAF. Après l'avoir installé ainsi que ses dépendances—Apache POI, principalement—et ignoré les tests automatiques qui échouent parce qu'ils ne gèrent pas l'heure d'été, je débute par un test très simple afin d'éviter de perdre du temps comme précédemment: ouvrir un AAF et l'enregistrer sans le modifier le moins du monde… et je récolte une StackOverflowError dans une méthode récursive, que mes compétences en Java et mes connaissances de AAF ne me permettent pas de régler rapidement. Peut-être que les AAF en question sont vraiment pourris? C'est possible, mais je remarque que Boue Première emploie le même SDK AAF que nous: il a beau être un peu archaïque, il est strict et les fichiers qu'il produit devraient être sémantiquement corrects, à défaut d'avoir un agencement exploitable par Belle Lumière.

Reste alors le dernier espoir: PyAAF2, qui est le moyen via lequel OTIO gère les AAF. PyAAF2 est en pur Python—donc nettement plus agréable et expressif que COM—mais n'est pas non plus très documenté, et j'avais déjà tenté de l'employer de manière infructueuse: il butait dans du code de bas niveau, dans la couche FAT, que je ne connais pas non plus. Cependant, son auteur semble très bien connaître AAF, et le projet est actif, j'ai donc tenté un git pull… et une mise à jour a été téléchargée, qui semblait avoir corrigé le problème que je rencontrais. Grâce à beaucoup de type(item), help(item) et dir(item), je finis par comprendre la structure des AAF exportés par Boue Première. Je peux dès lors la reproduire après avoir supprimé les pistes vidéos, modifié les chemins vers les sons, et surtout ajouté cette fameuse référence vidéo… sans problème. Je peux même ouvrir les AAF résultants dans le relativement strict Conception Magienoire Résoudre de Vinci, alors que ceux générés par notre outil interne mènent immédiatement à un SIGSEGV. Sans entrer dans des détails que je ne maîtrise pas, et de mémoire, ça se présente à peu près comme ça:

-- IAAFHeader
-- IAAFDictionary
-- IAAFContentStorage
   |-- IAAFMob (vecteur)

Les IAAFMob peuvent être des IAAFMasterMob, des IAAFSourceMob, des IAAFCompositionMob… mais je ne m'intéresse qu'aux deux derniers.

   |-- IAAFSourceMob (vecteur)
       |-- IAAFEssenceDescriptor (dont ceux qui m'intéressent sont des IAAFFileDescriptor)
           |-- IAAFLocator (vecteur, dont ceux qui m'intéressent sont des IAAFNetworkLocator)
   |-- IAAFCompositionMob (vecteur)
       |-- IAAFTimelineMobSlot (vecteur)
           |-- IAAFNestedScope
               |-- IAAFSequence
                   |-- IAAFSourceClip (contient des références vers les IAAFSourceMob)

▶ ⏺

Une fois un petit outil de 327 lignes de Python4 écrit dans mon temps libre, j'ai employé GNU parallel pour l'exécuter sur les quelques centaines de fichiers qui ont posé problème avant la mise en place du correctif. Ils ont tous été copiés et transformés sans encombre. Reste à tester le résultat dans Belle Lumière en présence d'un ingénieur du son. Ça sera au mieux mercredi, télétravail oblige.

Le code est trop long et probablement inutile pour tout le monde sauf moi-même, mais, grâce à Python, on reste dans quelque chose d'une grande simplicité:

class AAFTransform(object):
    ...
    def __str__(self):
        return "AAFTransform"

    def transform(self):
        # Using @abstractmethod would be a much better idea
        raise NotImplemented()

    def run(self):
        logging.info("Running {transform}".format(transform = str(self))
        return self.transform()

class AAFRemoveVideo(AAFTransform):
    # This class doesn't require too much external code, so let's paste it ;)
    def __init__(self, exceptions, project_id, aaf_file, aaf_filepath):
        super(AAFRemoveVideo, self).__init__(project_id,
                                             aaf_file,
                                             aaf_filepath)
        self.exceptions = exceptions

    def __str__(self):
        return "AAFRemoveVideo"

    def is_removable(self, slot):
        return (isinstance(slot, aaf2.mobslots.TimelineMobSlot)
                and isinstance(slot.segment, aaf2.components.NestedScope)
                and "Picture" == slot.media_kind
                and slot.name not in self.exceptions)

    def collect_slots(self, mob):
        # whenever a slot is popped, all the following slots are
        # shifted down, so we don't increase the index when we find
        # a removable slot, that way everything will fall into place
        current_index = 0
        indexes = []
        for slot in mob.slots:
            if self.is_removable(slot):
                indexes.append(current_index)
            else:
                current_index += 1
        return indexes

    def remove_slots(self, mob, indexes):
        try:
            for index in indexes:
                logging.info("Removing MobSlot \"{s}\"".format(
                             s=mob.slots[index].name))
                mob.slots.pop(index)
            return True
        except Exception:
            logging.error("Failure while trying to remove a slot.")
            return False

    def transform(self):
        top_mob = self.get_toplevel()
        indexes = self.collect_slots(top_mob)
        return self.remove_slots(top_mob, indexes)

class AAFInsertVideo(AAFTransform):
    ...

class AAFRelinkAudio(AAFTransform):
    ...

class AAFProcessor(object):
    ...
    def process_file(self, ...):
        try:
            self.copy_and_read_file()
            transform_ops = [AAFRemoveVideo(..., self.aaf_file, ...),
                             AAFInsertVideo(..., self.aaf_file, ...),
                             AAFRelinkAudio(..., self.aaf_file, ...)]
            if not all([op.run() for op in transform_ops]):
                logging.error("One or more transformations didn't go all the way.")
        except Exception:
            logging.error("Oops. I didn't quite catch that.")
        finally:
            self.save_and_move_file()

if "__main__" == __name__:
    AAFProcessor(aaf_file_name, ...).process_file(...)

Je ne prétends pas qu'il s'agisse de bon code, ni qu'il soit idiomatique, mais il est bref, fonctionne, et est relativement extensible.

Ironie du sort, chez nous, Python est considéré comme une sorte de GW-BASIC pour Linux, un jouet pour ceux qui ne sont pas de vrais programmeurs. Ça montre à quel point la télévision est en retard niveau informatique, et ce conservatisme explique peut-être en partie pourquoi elle semble suivre la voie tracée par GW-BASIC vers les tréfonds de la non-pertinence (désolé pour les aficionados de GW-BASIC).

S'il s'avère que ça fonctionne, il aura été démontré que Boue Première n'est pas la source du problème, mais qu'elle se situe plutôt au niveau du SDK5 ou de l'outil développé en interne. Dans ces deux cas, il reviendra à l'équipe d'intégration de trouver une solution, soit en contournant le bug du SDK, soit en corrigeant le code de son outil.

La suite dans un prochain journal, donc, si j'ai un peu de temps au boulot.


  1. Les fichiers vidéo, audio, de sous-titres… 

  2. C'est du moins le comportement qui serait induit par le connecteur reliant le montage au bus. 

  3. il l'est probablement, mais je n'ai pas eu le courage de lire la licence jusqu'au bout. Elle est lisible à l'adresse https://github.com/dneg/aaf/blob/master/LEGAL/AAFSDKPSL.TXT

  4. Selon SLOCCount

  5. Douteux, car pratiquement tous les logiciel qui prennent en compte AAF l'utilisent, et il n'a pas beaucoup bougé depuis des années. 

  • # Moi aussi j'utilise Belle Lumière

    Posté par  (site Web personnel) . Évalué à 4.

    C'est intéressant parce que moi aussi j'utilise Belle Lumière pour du mixage de sujets pour le journal télévisé en couleur. Par contre les aaf sont générés par Assembleur de Medias Affamé, mais dans le même genre que toi (médias linkés).

    Chez nous, l'équipe d'intégration a eu accès à une sorte d'API de Belle Lumière, et c'est donc Belle Lumière, piloté par une application maison, qui effectue l'import aaf (de la même manière que si on utilisait les menus de l'application, un peu comme une macro).

    Y'a un truc qu'il aime pas du tout, c'est quand par mégarde un clip audio dépasse à la fin au dela de la dernière image vidéo dans la timeline de montage.

    (Et je soupçonne que c'est ffmpeg qui derrière wrappe les fichiers wav mixés dans le MXF XDCamHD qui contient l'essence vidéo avant de pousser vers le serveur de diffusion).

    • [^] # Re: Moi aussi j'utilise Belle Lumière

      Posté par  . Évalué à 3.

      C'est amusant de tomber sur des gens qui bossent dans le milieu. :) Juste une précision: je n'utilise pas Belle Lumière. En tant qu'informaticien, je me contente de rendre la vie de ses utilisateurs insupportable.

      Nous avons aussi obtenu le SDK de Belle Lumière, du moins si on parle bien du Belle Lumière d'avant le Résolveur de Vinci. Avant ça, nous bossions également avec Assembleur de Médias, mais l'intégration avait été réalisée par une société tierce. Ça fonctionnait assez bien, d'ailleurs.

      Vous n'avez pas trop de problèmes avec le matériel qui vieilli? Les Constellation, EVO, $@#¤! de Xynergi, les interfaces SX-*..?

      Chez nous, toujours, FFmpeg générait des fichiers MXF XDCAM HD422 qui n'étaient que moyennement appréciés par le diffuseur de régie finale et par l'outil de QA en amont… On aura plutôt tendance à employer FFmbc, même s'il est très probable que FFmpeg ait nettement progressé sur ce front. Mais, oui, c'est très probablement FFmpeg ou FFmbc qui wrappe les fichiers. C'est d'ailleurs FFmpeg qui était en partie derrière Codeur Carbone de Rosette. Il n'y a plus que très peu de boîtes qui développent des codecs (je ne vois vraiment que N00b et Concept Principal…)

      • [^] # Re: Moi aussi j'utilise Belle Lumière

        Posté par  (site Web personnel) . Évalué à 1. Dernière modification le 13/09/20 à 17:39.

        Toujours avec le Belle Lumière d'avant le Résolveur de Vinci, et du matériel EVO qui effectivement vieillit ;-).

        Prochainement, dans les mois qui viennent, migration vers Belle Lumière dans Résolveur de Magie Noire, en renouvelant le hardware. Je suis curieux de voir l'intégration SX-36 et CC-2 avec cette version.

  • # Générateurs python

    Posté par  (site Web personnel) . Évalué à 9.

    Au lieu de

    transform_ops = [
        AAFRemoveVideo(..., self.aaf_file, ...),
        AAFInsertVideo(..., self.aaf_file, ...),
        AAFRelinkAudio(..., self.aaf_file, ...)
    ]
    
    if not all([op.run() for op in transform_ops]):
        logging.error("One or more transformations didn't go all the way.")

    tu devrais utiliser la syntaxe

    transform_ops = [
        AAFRemoveVideo(..., self.aaf_file, ...),
        AAFInsertVideo(..., self.aaf_file, ...),
        AAFRelinkAudio(..., self.aaf_file, ...)
    ]
    
    if not all(op.run() for op in transform_ops):
        logging.error("One or more transformations didn't go all the way.")

    Ce qui change, c'est qu'au lieu de générer une liste contenant le résultat de toute les ops et qui sera ensuite parcourue par la fonction all(), la fonction all() va pouvoir directement évaluer le résultat de tes opérations. Ce qui fait que tu te passes de la création d'une liste temporaire, et tu stops le traitement dès la première erreur rencontré. Après, si ta liste est petite et que tes traitement sont rapides, limite osef. Mais comme ça marche aussi bien pour les petits cas que les grands cas, autant prendre l'habitude de l'utiliser partout ;)

    # Un exemple plus succinct
    
    # Ce code va générer un tableau de 1M éléments
    all([i != 1 for i in range(1000000)]
    
    # Ce code va générer seulement 2 éléments
    all(i != 1 for i in range(1000000))
    • [^] # Re: Générateurs python

      Posté par  . Évalué à 3.

      Merci, j'ignorais cette subtilité! :) Par contre, je veux bel et bien que toutes les opérations se déroulent, même si une erreur survient quelque part. Dans mon secteur, il vaut mieux un AAF foireux arrivé à l'heure que pas d'AAF.

    • [^] # Re: Générateurs python

      Posté par  . Évalué à 6.

      Benchmarkons :

      In [2]: %timeit all([i != 1 for i in range(1000000)])
      68.5 ms ± 1.93 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
      
      In [3]: %timeit all(i != 1 for i in range(1000000))
      883 ns ± 10.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
      
      

      Mais pour les listes courtes, c'est normalement pas pareil :

      In [4]: %timeit all([i != 1 for i in range(1000)])
      68.5 µs ± 275 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
      
      In [5]: %timeit all(i != 1 for i in range(1000))
      902 ns ± 13.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
      

      Ah ben si en fait.

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n’en sommes pas responsables.