Recréer Netflix dans son garage

Il y a quelques temps, je me suis intéressé à l’architecture utilisée par Netflix pour diffuser du contenu à si grande échelle. Les ressources que cela nécessite n’étant pas à ma portée, je me suis également intéressé à la manière dont une startup pourrait procéder, pour proposer un service vaguement équivalent, mais à un coût beaucoup plus réduit.

En réalité, la problématique est très proche de celle des IPTV: comment diffuser des médias, à la demande ou en flux, sur un large territoire, via Internet, sachant que les consommateurs sont des ordinateurs plus ou moins embarqués (navigateurs Web, télévisions connectées, petits boîtiers de type Raspberry Pi)? J’ai donc décidé de me pencher sur les problèmes de mise en œuvre de ce type de produit.

La situation se présente comme suit: un diffuseur de contenu cherche un canal de distribution efficace et peu onéreux, capable d’exploiter une infrastructure moderne (Internet, via la fibre mais aussi la 5G) afin d’apporter aux clients un service de qualité similaire à celui d’entreprises telles que Netflix ou Disney+.

Dans ce bille5, nous nous concentrerons sur l’extrémité de la chaîne de diffusion: l’approvisionnement locale d’un lecteur de média en contenu, et remonterons vers la source de contenu: le distributeur.

Je ne fais l’apologie ni du partage, ni de la consommation de contenu illégal, et n’y ai moi-même pas recours. Mon travail est motivé par une curiosité concernant les aspects techniques de mise en œuvre de la diffusion de média à grande échelle et à coût réduit. La diffusion de contenus protégés, notamment contre rémunération, doit faire l’objet de travaux légaux et commerciaux qui sortent de mon cercle d’intérêt.

Le dernier maillon: Kodi

La diffusion de contenu peut être vue comme une longue chaîne qui commence par la capture du média (sa création) et se termine par sa consommation par l’utilisateur. Je préfère commencer par la fin de la chaîne afin de prendre en compte les besoins techniques du client, et remonter progressivement jusqu’à son origine afin de collecter les contraintes intermédiaires et envisager des solutions qui seront cohérentes de bout en bout.

Créer un lecteur multimédia ex nihilo demande un effort titanesque, ne serait-ce que pour obtenir un support décent des nombreux codecs permettant la consommation de médias présentant une grande variété d’encodage. L’effort est d’autant moins pertinent qu’il existe des solutions libres et gratuites déjà disponibles. Les navigateurs Web permettent la lecture de médias dans certains formats. VLC est un lecteur multimédia extrêmement performant et polyvalent.

Kodi est un centre multi-média: il s’agit d’une application vous présentant vos collections de média (films, séries, musiques, radios, jeux, …) avec l’affiche et le résumé de chaque entrée. En un clic, la lecture du média se lance.

L’apparence de Kodi est grandement personnalisable, avec des « skins » à télécharger ou à créer soi-même. Kodi s’exécute sur une grande variété de plate-formes, alors de l’ordinateur à des systèmes mono-carte, en passant par le smartphone.

Présentation de Kodi sur différents supports

C’est Kodi que j’ai retenu, pour ses capacités d’extension et la qualité de la présentation de « bibliothèques » multimédia. Lors de l’installation de Kodi, l’utilisateur peut spécifier des sources de média, par exemple des disques durs locaux, des serveurs NAS ou encore des serveurs HTTP ou WebDav. C’est cette dernière option qui m’intéresse, car la possibilité de consommer du contenu desservi par un serveur HTTP donne la possibilité de gérer un catalogue distant et géré par le fournisseur de contenu.

Encodage des médias

L’encodage des médias est un point clef de la réussite du système de distribution. Si une musique ne pèse que quelques Mo en mp3 et peut donc être téléchargée en quelques secondes sans trop se prendre la tête, il faut penser aux chapitres de livres audio, qui peuvent durer des dizaines de minutes, et a fortiori aux longs métrages qui peuvent durer plusieurs heures. Je m’attarderai ici aux longs métrages qui présentent le plus d’enjeux techniques.

Tout encodage représente un compromis entre des besoins contradictoires. Ceux-ci comprennent

  • Une grande qualité tant du son que de l’image, avec une demande sans cesse plus exigeante de l’utilisateur (5.1, 4K, HDR, etc.)
  • Une bande passante réduite et fluctuante, bien que le temps tende à améliorer cet aspect (4G puis 5G, xDSL puis fibre optique, etc.)
  • Des supports de consommation de plus en plus variés, du smartphone à l’écran géant, en passant par la tablette.
  • Des coûts de transport et de stockage réduits.
  • Des ressources de décodage contraintes (accélération matérielle, etc.).

Pour le confort de l’utilisateur, la lecture doit commencer au plus tard quelques secondes après le choix du média. Elle doit se produire sans interruption. L’utilisateur doit changer la langue et les sous-titres en cours de visionnage, et le changement doit être effectif de manière transparente.

Il n’est pas envisageable d’avoir à télécharger l’entièreté du média (ce qui prend au minimum plusieurs minutes dans le cas dun long métrage) avant de commencer sa lecture. Il faut donc découper le film en blocs, et rendre chaque bloc aussi indépendant que possible. De plus, il faut une grande prédictibilité de l’ordre de consommation des blocs, afin que le système sache quels seront les blocs devant être téléchargés en priorité en vue de leur lecture sans attente.

Encodage vidéo

Le codec AV1 me semble un compromis globalement satisfaisant entre ces compromis. Pour notre cas d’usage, il est de préférence utilisé conjointement à une variante du format de conteneur MP4 appelée fMP4. Dans cette variante, le média est divisé en fragment ou segment, chacun pouvant être lu indépendamment des autres pour autant qu’un premier segment, dit d’initialisation, ait été lu au préalable. ffmpeg est un véritable couteau suisse du multimédia, et gérera facilement l’encodage.

Chaque segment représente quelques secondes de film et pèse typiquement entre 1 et 10 Mo, ce qui le rend facilement et rapidement téléchargeable en un seul bloc. Si les segments représentent 6 secondes, un film de 2 heures est constitué de 1200 segments vidéo. Ce nombre, conséquent sans être disproportionné, facilite la distribution du média.

AV1 est assez demandeur en termes de ressources. Vérifiez néanmoins si votre système cible propose un décodage matériel, ou à défaut dispose d’un processeur assez performant pour un décodage logiciel.

C’est moins glorieux pour l’encodage. Le support matériel est assez récent, et l’architecture Silicium d’Apple (processeurs M) ne le supporte pas du tout. Vous serez dans ce cas limité à un encodage logiciel, typiquement 20-30 fois plus lent. Ceci dit, un encodage logiciel permet actuellement un meilleur encodage, car le logiciel peut jouer avec des subtilités de AV1 qui ne sont pas accessibles au matériel.

Adaptive Bitrate Streaming

Afin de proposer une solution robuste face aux différences en terme de bande passante, ainsi que face à la variation de cette bande passante dans certains réseaux, il est courant de proposer plusieurs encodages de la même piste vidéo. C’est une technique connue sous le nom d’ABR (Adaptive Bitrate Streaming).

On l’encode par exemple en 360p (solution de dernier recours, si le débit est très mauvais), 480p, 720p (bien pour les smartphones), 1080p (convient pour une tablette ou une télévision) et 2160p (utilisable dans de bonnes conditions). Les segments sont alignés: ils commencent et se terminent au même instant pour toutes les résolutions. Ainsi, si l’utilisateur (ou directement Kodi) décide de changer la résolution, cela peut se faire lors du passage d’un segment au suivant, sans saut temporel. L’utilisateur percevra simplement une différence dans la qualité de l’image ainsi que dans sa fluidité.

Séparation des pistes

Les différents encodages de la piste vidéo, ainsi que les pistes audio encodées en AAC, sont enregistrés dans des fichiers distincts, typiquement nommés seg_X_YYYY.m4s, où X est le numéro de piste, et YYYY est le numéro de segment. Cette séparation présente plusieurs avantages.

Tout d’abord, l’utilisateur ne doit télécharger que les pistes qui l’intéressent. Dans un cas typique, seules les pistes audio correspondant à sa langue maternelle et à la langue originale du film sont susceptibles de l’intéresser. De même, si l’utilisateur souhaite une visualisation en 4K, autant éviter de télécharger toutes les résolutions plus faibles, plutôt qu’augmenter la consommation de bande passante. C’est encore plus vrai dans le cas où Kodi privilégie une résolution faible, et où l’économie de bande passante devient cruciale pour le confort d’utilisation.

Ensuite, la séparation de la vidéo et de l’audio évite d’avoir à encoder chaque combinaison de résolution et de langue, ce qui réduit la complexité opérationnelle ainsi que les coûts de stockage. La contrepartie, à savoir le besoin de synchroniser les deux pistes lors de la lecture, est très bien gérée par un lecteur tel que Kodi.

Le point d’entrée: un proxy HTTP

Le contenu consommé ne peut être local. Le catalogue du fournisseur est potentiellement très conséquent: pensez au volume de stockage représenté par l’ensemble des films de Netflix. Le fournisseur enrichit continuellement ce catalogue, et en supprime parfois des entrées au gré des accords commerciaux.

On souhaite également que le client doive valider régulièrement sa souscription au service. Disposer de larges parts du catalogue présente le risque que le client puisse continuer à consommer des médias sans souscription en ordre. Pire: si le client dispose de larges parts du catalogue, le risque augmente que celui-ci soit partagé sans contrôle du système de distribution.

Pour ces raisons, l’architecture repose, du côté du client, par une application Kodi exploitant le catalogue exposé par un serveur HTTP appelé proxy. Ce proxy s’exécute sur la même machine que Kodi et assure les fonctionnalités élémentaires suivantes:

  • Communication avec le serveur du distributeur pour actualisation du catalogue.
  • Récupération du contenu sur Internet.
  • Fourniture du contenu à Kodi, sur un port HTTP écoutant localement.

Structure de fichiers

Pour le catalogage, le proxy expose, via un service HTTP, un répertoire movies contenant, pour chaque film, un fichier <UUID>.strm et un fichier <UUID>.nfo.

Cela est contraire aux bonnes pratiques proposées par Kodi, qui suggèrent d’utiliser le titre du film comme nom de fichier, et de placer les fichiers de chaque film dans un répertoire dédié. Cette bonne pratique permet principalement à Kodi de deviner le contenu du média, et à télécharger les bonnes métadonnées (poster, résumé, etc.) sur Internet.

Cependant, le découplage du titre du film du nom des fichiers facilite la modification du contenu: si on souhaite réencoder le média, ou proposer plusieurs éditions d’un même film, il suffit d’utiliser des UUID différents. Un autre avantage est que le proxy n’a pas à adapter le titre du film à la langue de l’utilisateur.

Le fichier strm est un fichier textuel indiquant à Kodi qu’il s’agit d’un média streamé, ainsi que l’emplacement du fichier master, véritable point d’entrée de la lecture d’un flux.

#KODIPROP:inputstream=inputstream.adaptive
http://localhost:$port/media/UUID/master.mpd

Le fichier nfo, lorsqu’il est présent, est utilisé afin d’indiquer à Kodi les métadonnées relatives au média. Cela évite le risque de mal estimer le contenu dont il est question à partir d’un titre parfois ambigu. Le fichier nfo est généralement un XML respectant une structure moyennement documentée. Une partie de la valeur de notre solution peut donc consister en une base de données des posters, acteurs, réalisateurs, résumés, etc. de chaque film du catalogue, et ce dans toutes les langues supportées. On peut opter pour une autre approche, selon laquelle le fichier contient en tout et pour tout une URL vers l’entrée TMDB du film. C’est alors le scraper de Kodi qui fait le travail de collecte des métadonnées.

https://www.themoviedb.org/movie/xxxx

Avec ces seules informations, Kodi est capable de présenter agréablement le catalogue de films, comme le ferait l’application de Netflix. De temps à autre (selon la configuration), Kodi consulte le proxy et actualise sa propre base de données afin de prendre en compte les films ajoutés et supprimés.

Le proxy doit également fournir les fichiers de contenu proprement dits. On peut les placer dans http://localhost:$port/movies/UUID/.

Le fichier master.mdp, propre au streaming adaptatif MPEG-DASH, est le manifeste du média. Il s’agit d’un fichier XML, typiquement de 200 Ko pour un long métrage encodé comme décrit ci-dessus. Il contient les informations nécessaires à la lecture du film, et principalement les pistes qui le composent. Pour chaque piste, la qualité d’encodage est précisée, ainsi que le codec utilisé et la langue de la piste (pour les pistes audio). Cela permet au lecteur de décider quelles pistes lire. Un fichier d’initialisation est également mentionné, ainsi que la longueur, en frames, de chaque segment. Le fichier d’initialisation (typiquement, de 1 Ko) doit obligatoirement être lu avant de lire le reste de la piste. Le détail des segments permet au lecteur de lire des parties arbitraires de la piste, plutôt que de devoir la lire à partir du début.

Détail intéressant, il est possible de préciser que la liste des segments est incomplète. Dans ce cas, Kodi consulte régulièrement le fichier master afin de prendre connaissance des nouveaux segments disponibles. Cela est utile lorsque le média n’est pas un film ordinaire, mais un véritable flux audio-vidéo, par exemple une diffusion télévisée en direct.

En plus du fichier master et des fichiers d’initialisation, le répertoire du film contient les segments des pistes. La taille de ceux-ci dépend de la nature du média, de la qualité d’encodage et de la longueur du segment. Bien que Kodi puisse utiliser des Range requests, en pratique les segments semblent toujours être récupérés en entier.

TypeTaille typique
Vidéo 4K10 Mo
Vidéo 1080p2 Mo
Vidéo 360p200 Ko
Audio100 Ko
Sous-titre1 Ko
Ordre de grandeur de la taille des segments d’approximativement 6 secondes, par type.

En résumé, le proxy propose, via un service HTTP, un ensemble de fichiers relatifs au film. Kodi va se baser sur le fichier master pour savoir que lire, sur les fichiers d’initialisation pour savoir comment lire, et sur les fichiers de segment pour la lecture proprement dite.

Le proxy donne à Kodi l’illusion que l’ensemble des fichiers sont immédiatement disponibles à la lecture. Le point clef du confort d’utilisation de la solution consiste à maintenir cette illusion, bien que les fichiers doivent en réalité être récupérés sur le réseau. En plus de s’intégrer dans un réseau de distribution efficace, le proxy devra donc gérer efficacement le cache des fichiers, voire anticiper les demandes à venir afin de pouvoir les satisfaire plus rapidement.

Sachez que Kodi gère lui-même un cache et tente de maintenir une avance de quelques dizaines de seconde sur la position de lecture. S’il ne parvient pas à conserver cette avance, il passera automatiquement sur un flux vidéo moins exigeant, au détriment de la qualité d’image.

Quelle techno pour le proxy?

Le proxy constitue une brique logicielle centrale dans notre architecture. Il est le relais entre le distributeur et le lecteur et, à ce titre, concentre une bonne part de la plus-value de la solution. On cherche donc à rendre sa modification difficile.

Il est amené à s’exécuter sur une variété de plate-formes aux ressources parfois limitées. De plus, il est soumis à des contraintes temps réelles molles. Cela donne l’avantage aux technologies performantes, idéalement compilées, proposant un traitement efficace des flux de données.

Rust répond assez bien à ces besoins. J’ai eu l’occasion de réaliser un prototype de proxy dans ce langage. Son modèle de concurrence, l’absence d’un garbage collector pouvant introduire une latence variable, ainsi que sa faible empreinte mémoire à l’exécution, m’ont particulièrement séduit. Les performances se sont avérées plus que satisfaisantes durant les tests.

Un réseau de distribution en pair à pair

Bien que le contenu multimédia ne soit initialement pas disponible localement, la présence d’une partie du catalogue sur un espace de stockage local reste malgré tout préférable. Tout d’abord à des fins de cache, comment nous venons de le dire: Kodi pourra ainsi consommer le contenu dans de bonnes conditions, sans risquer que la lecture ne soit interrompue en raison d’un problème de connectivité du client.

Cela économise également les ressources en cas de second visionnage: le média étant déjà présent localement, Kodi peut le consommer sans avoir à le re-télécharger. Seul un bref accès à un serveur distant peut s’avérer nécessaire pour obtenir les éléments nécessaires à la consommation (une permission, une clef de déchiffrement, etc.).

Une troisième justification à l’usage d’un espace de stockage local est la capacité qu’a alors chaque client de redistribuer le contenu. En effet, dans une approche centralisée, la distribution de contenus volumineux (tels que des films) repose sur un ensemble de serveurs hébergés par le distributeurs et épaulés par un CDN efficace. Cela implique des coûts d’infrastructure conséquents, même pour un réseau de distribution de faible envergure. Les clients disposent typiquement d’une capacité de transmission du contenu bien moindre qu’un serveur ordinaire ou qu’un CDN, mais en contre-partie ils se partagent les coûts et la charge de distribution, ce qui permet au distributeur d’augmenter la taille de son réseau sans augmenter significativement ses coûts.

Le proxy doit donc prendre le rôle de pair dans un réseau pair à pair afin de contribuer à la diffusion du contenu.

Il est possible de concevoir de tels réseaux de manière totalement décentralisée, par exemple en utilisant une table de hachage distribuée. Cela n’est cependant pas approprié à notre usage, car le distributeur souhaite garder le contrôle de l’échange du contenu. Notre réseau sera donc basé sur un serveur, appelé tracker, sous le contrôle du distributeur et indiquant les pairs proposant le contenu souhaité. Les clients ne pouvant accéder à un contenu donné (parce que leur souscription ne le leur permet pas) n’obtiendront pas de liste de pairs pouvant satisfaire à leurs demandes.

La séparation des pistes des médias, évoquée plus haut, est ici un avantage. Elle permet de mutualiser la piste vidéo, qui est indépendante de la langue. Autrement dit, le flux vidéo sera partagé par l’ensemble des consommateurs, et seuls les flux audio et de sous-titrage, beaucoup moins volumineux, ne seront partagés qu’au sein d’une communauté linguistique. En conséquence, le nombre de pairs pouvant contribuer à la distribution d’un média est optimisé, ce qui contribue à la fluidité du téléchargement.

Bootstrapping et disponibilité minimale

Si l’exploitation des pairs réduit considérablement l’infrastructure nécessaire à la distribution du contenu et permet un passage à l’échelle du réseau de distribution, elle ne résout pas tous les problèmes. Les contenus peu populaires ou récents ne sont desservis que par peu de pairs, voire par aucun. Les pairs peuvent se retirer à tout moment du réseau. De manière générale, le distributeur doit assurer la disponibilité du contenu même lorsqu’aucun consommateur n’est capable de le faire pour lui.

Une réponse classique à cette problématique consiste en une approche hybride, où le réseau pair à pair est complété par des serveurs ou du CDN. Cependant, afin de réduire les coûts et de garder une simplicité opérationnelle, le distributeur peut également provisionner des pairs hébergés dans des data centers. Ces pairs spéciaux, dits seed nodes ou origin nodes, disposent d’une grande bande passante, s’engagent à rester disponibles et à proposer de larges pans, voire l’intégralité, du catalogue du distributeur. Ils sont répertoriés par le tracker au même titre que les pairs des clients, si bien que ceux-ci peuvent les contacter pour récupérer du contenu.

Ainsi, le principal coût opérationnel pour le distributeur sera lié à l’hébergement et au provisionnement des seed nodes nécessaires pour assurer un bon bootstrapping du contenu ainsi que sa disponibilité minimale.

Architecture pair à pair ad hoc

Dès qu’on parle de réseau d’échange en pair à pair, il est inévitable de penser à BitTorrent. Le protocole a en effet de nombreux arguments: il est très mature, dispose de nombreux clients, et bénéficie d’une adoption massive. Son recours pour le partage illégal d’œuvres protégées lui donne cependant une réputation sulfureuse, qui pourrait rebuter les distributeurs.

Par nature, il présente également plusieurs inconvénients. Son protocole repose sur la récupération prioritaire des fichiers (ou pièces) les plus rares, afin de maximiser la disponibilité du contenu. Cependant, dans notre cas, il est plus important d’obtenir les fichiers qui sont sur le point d’être consommés, afin d’éviter la rupture de la lecture. La gestion des priorités au téléchargement par le proxy est facilitée par le recours à un protocole ad hoc.

BitTorrent a été conçu pour l’échange de fichiers volumineux, tels que des images iso. Il est généralement nécessaire d’attendre la récupération de tous les fichiers afin de considérer le téléchargement comme réussi. Inversement, notre repose sur une multitude de fichiers de quelques mégaoctets, qui doivent être lus à temps et dans un ordre précis. Cela renforce l’intérêt d’un protocole ad hoc.

Sécurisation du contenu

Le distributeur de contenu souhaite naturellement éviter que celui-ci soit librement échangeable et lisible. Un business model classique consiste à exiger du consommateur une souscription payante en échange de l’accès au catalogue du distributeur. Plusieurs mécanismes complémentaires sont envisageables.

L’usage de DRM durs ne peut être retenu. Tout d’abord parce qu’ils reposent sur un matériel sécurisé, alors que nous visons une consommation sur des plate-formes variées, donc hors de notre contrôle, et le plus souvent en grande partie logicielles. Ensuite parce que cette approche impose soit un procédé cryptographie figé, qui, une fois circonvenu, n’est plus d’aucune utilité, soit sur la distribution de tokens physiques (par exemple, une carte ou une clef usb), ce qui limite la dématérialisation de la solution et engendre des coûts logistiques qu’on souhaite éviter.

Le premier niveau de sécurité repose simplement sur le proxy, qui n’expose de contenu au lecteur qu’après avoir vérifié la validité de l’abonnement de l’utilisateur.

Un pilier de notre sécurisation repose sur le rôle central du distributeur. Ce n’est qu’après une authentification et une vérification des permissions que le proxy peut mettre à jour son catalogue. Celui-ci peut d’ailleurs être segmenté (en qualité, par langue, par type de média, etc.) selon le profil de l’utilisateur. On peut par exemple réserver les versions 4K aux abonnés premium. Cette approche peut être raffinée en proposant des médias « à la demande », c’est à dire dont l’accès n’est autorisé qu’après paiement spécifique, et éventuellement pour une courte période.

Le tracker est un autre point central, et contribue à la sécurisation: un proxy ne peut obtenir de liste d’autres nœuds capables de partager un média que si l’utilisateur peut légitiment consommer ce média. Un utilisateur dont l’abonnement est suspendu se retrouvera ainsi rapidement incapable de récupérer du contenu qui n’est pas caché par son proxy.

Les autres nœuds, pourvoyeurs de contenus, contactent également le tracker afin de confirmer la légitimité des demandes. Ceci évite qu’un proxy trop entreprenant ne démarche spontanément des nœuds préalablement identifiés.

Les segments de médias sont chiffrés, avec une clef propre à leur média. Chaque segment est chiffré indépendamment des autres, de sorte qu’on puisse déchiffrer les segments individuellement. Seule la version chiffrée des segments est partagée. Le média n’est ainsi jamais exposé en clair,  ni dans un cache, ni lors des échanges entre nœuds. Lorsque le lecteur demande un segment au proxy, celui-ci demande la clef appropriée au distributeur, qui ne la fournit qu’après vérification de la légitimité de la demande. Le proxy déchiffre alors le segment et l’expose au lecteur. Le besoin de cacher la clpef, ne serait-ce qu’à court terme, et le fait que la transmission du segment entre le proxy et le lecteur, constitue une double menace. Certains lecteurs supportent certaines formes de chiffrement, ce qui atténue cette menace.

Un mécanisme de rotation des clefs peut être prévu. Lorsque celui-ci est activé, le distributeur rechiffre un média avec une nouvelle clef, et modifie son catalogue afin que le même média fasse référence à la nouvelle version des segments plutôt qu’à l’ancienne. Cela rend sans valeur la possession de l’ancienne clef si celle-ci avait fuité. Cependant, cela invalide également le cache des anciens segments du média, et réduit donc la disponibilité de ce dernier le temps que la nouvelle version des segments se répande au sein des nœuds.

Conclusion

Difficile d’être sérieux quand on parle de recréer Netflix dans son garage. Mais malgré la démesure des moyens dont dispose ce géant, des concurrents tentent de le détrôner, et des alternatives spécialisées veulent s’accaparer des niches où tout n’est pas encore dit.

Dans le même temps, le domaine est tellement dématérialisé et la technique évolue tant qu’il est possible de compenser une arrivée tardive sur le marché par un avantage sur la mise en œuvre.

Dans ce billet, j’ai tenté de proposer une approche qui écrase les coûts opérationnels. Loin d’une innovation de rupture, je me base sur des outils existants et la démocratisation des réseaux informatiques haut débit pour esquisser un système de distribution multimédia qui tienne un minimum la route.

Ça a été pour moi une occasion de comprendre certains aspects des flux vidéo et du fonctionnement de Kodi, mais aussi de m’initier à Rust.

Laisser un commentaire