Résumé : si t'as un site web de diffusion de vidéos qui utilise Django et videojs, que les vidéos sont longues à charger voire que les longues vidéos sont remplacées par une erreur « The quota has been exceeded », vérifie que ton frontal Apache httpd ne proxifie pas les requêtes HTTP portant sur les vidéos à l'application Django car ce dernier ne prend pas en charge le contenu partiel (entête HTTP « Range »). Profite-en pour surcharger la variable « DEBUG » dans settings_local.py, car elle veut True par défaut…
Nous avons un site web qui diffuse des vidéos. Il s'agit d'un logiciel Python Django derrière un serveur d'applications uwsgi derrière un frontal Apache httpd.
Quand une vidéo dépasse 20 mo (environ), elle n'est pas jouée et l'erreur « The quota has been exceeded » s'affiche à la place. Avec Firefox, car Chromium affiche rien. La console des outils de développement affiche « VIDEOJS: ERROR: (CODE:-3 undefined) The quota has been exceeded. Object { code: -3, type: "APPEND_BUFFER_ERR", message: "The quota has been exceeded.", originalError: DOMException } video.js:128:5 ».
Sur une vidéo plus petite, l'onglet réseau des outils de développement montre que le chargement de la page s'est effectué en 52 secondes, et qu'un fichier vidéo de 16 Mo a été récupéré. La lenteur est le deuxième problème signalé par nos utilisateurs. Forcément, si le navigateur web charge automatiquement l'intégralité de la vidéo en arrière-plan…
Sur le web, on trouve ces erreurs dans des tickets sans solution ici ou là.
La collègue développeuse change le code du logiciel pour que ffmpeg
découpe chaque vidéo en fragments (dit autrement : virer l'option « -hls_flags single_file »). Hop, plus d'erreur et la page web se charge en 600 ms. En parallèle, elle interroge les devs de l'application et la communauté : comme d'hab, personne a ce problème.
Mais ce découpage des vidéos n'améliore pas les choses avec Safari qui continue de lire en boucle les deux premières secondes de la vidéo, comme quand les vidéos n'étaient pas découpées.
C'est ici que j'arrive. Je trouve bizarre que les vidéos longues ne fonctionnent pas chez nous alors que ça marche chez les autres utilisateurs du logiciel.
À ce stage on a éliminé l'applicatif des causes potentielles (sa configuration reste une cause potentielle).
Un collègue nous donne une piste : le serveur ne gère pas l'entête HTTP « Range » dans les requêtes, alors que les vidéos sont distribuées sous forme de listes de lecture (playlists) indiquant une série d'intervalles de deux secondes au sein d'un même fichier.
En effet, nos listes de lecture (générées par ffmpeg
) ressemblent à ça :
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:2.000000,
#EXT-X-BYTERANGE:178412@0
360p.ts
#EXTINF:2.000000,
#EXT-X-BYTERANGE:211312@178412
360p.ts
[…]
Fragments de deux secondes (EXTINF). Le premier fragment commence à l'octet 0 du fichier 360p.ts et occupe 178412 octets. Le deuxième commence à l'octet 178412 du fichier 360p.ts et occupe 211312 octets.
Un test avec curl
confirme cette analyse :
$ curl -s -o /dev/null -D - -r 1315624-1562279 https://monorganisation.example/media/videos/ef2218bee799524931d2af11aafc3933e5d7413da2989b70fc94a9941fcd50e1/666/720p.ts
HTTP/1.1 200 OK
Date: Wed, 30 Sep 2020 17:10:42 GMT
Server: Apache/2.4.25 (Debian)
Content-Language: fr
Last-Modified: Wed, 16 Sep 2020 14:19:04 GMT
X-Frame-Options: EXEMPT
Vary: Accept-Language,Cookie
Content-Type: video/MP2T
Content-Length: 16929400
Cache-Control: max-age=0, no-store
Accept-Ranges: bytes
Le serveur nous a répondu avec le code HTTP retour 200 au lieu du 216 Partial Content, ce qui signifie qu'il ignore l'entête « Range ». On notera également que la taille annoncée (« Content-Length ») est la totalité du fichier et que l'entête « Content-Range » (qui rappelle la plage demandée sur la taille totale) est absent. On notera que MDN se trompe : la présence de l'entête « Accept-Ranges » ne suffit pas pour garantir qu'un serveur web honore bien l'entête HTTP « Range ».
Je sèche : la prise en charge du contenu partiel par les serveurs web est vieille… C'est grâce à cela que les gestionnaires de téléchargement, qui ouvrent plusieurs téléchargements simultanés d'un même fichier sur des plages d'octets différentes et qui proposent la reprise d'un téléchargement interrompu, fonctionnent. C'est forcément géré nativement par Apache httpd. Une recherche web et un essai sur plusieurs de nos serveurs me confirment cela.
À ce stade, le serveur web est éliminé. Reste sa configuration.
Dans certains résultats de recherche, on préconise d'ajouter l'entête « Accept-Ranges » ou « Range » avec le mod_headers… Ça a aucun sens (à ce stade, le traitement de la requête est terminé, Apache httpd a déjà répondu qu'il ne prenait pas en charge « Range » + « Accept-Ranges » est un entête de requête, pas de réponse, et le navigateur web la positionne déjà et il n'est pas retiré en chemin…), et, de toute façon, ma collègue a déjà tout tenté avec l'ajout d'entêtes, me dit-elle.
Je teste avec curl
: nos autres serveurs Apache httpd prennent en charge « Range ». Ils sont en version 2.4.25, comme le serveur problématique. À l'exception de l'hôte virtuel, toute la configuration est celle par défaut de Debian.
Le serveur est récent donc on ne mitige pas la vulnérabilité CVE-2011-3192 puisqu'elle est corrigée dans le code depuis longtemps. De même, Debian n'incorpore pas une directive MaxRanges restrictive.
À ce stade, la configuration du serveur web est éliminée, à l'exception du virtual host.
L'application est livrée avec une configuration (hôte virtuel) nginx. Le chef de mon équipe avait demandé à la collègue (d'une autre équipe) d'utiliser Apache httpd au motif qu'on avait uniquement des httpd, qu'on n'avait pas le temps pour s'éparpiller pour monter en compétence sur un autre logiciel, etc. Je trouve ça comique, le "si tu utilises nginx => pas d'aide de notre part ; si tu utilises apache => débrouille-toi pour faire l'intégration de l'appli". Je teste nginx et la configuration fournie par le projet sur le serveur de test : ça marche directement.
Si Apache httpd gère nativement le contenu partiel et si ça fonctionne de base avec nginx avec la configuration fournie par l'application, ça signifie qu'il y a une erreur dans la configuration de l'hôte virtuel pour Apache httpd conçue à partir de la conf' nginx fournie. Je relis, je compare avec la version nginx, et… bingo ! On a oublié d'exclure le chemin « /media » de la liste des chemins qu'il ne faut pas proxifier vers l'application Django.
Évidemment, il y avait aucun indice dans le journal Django. Quant à lui, le journal uwsgi mentionnait juste une requête reçue, qu'elle a été traitée en tant de temps, etc. Rien qui soit de nature à m'alerter.
Donc les requêtes sur les fichiers vidéos sont proxyfiées vers l'application Django. Le chemin vers lesdites vidéos est routé (on constate cela dans le fichier url.py de l'applicatif)… Mais Django ne prend pas en charge nativement le contenu partiel… Tout s'explique.
Au final, il faut ajouter ces directives dans l'hôte virtuel Apache httpd afin que toutes les requêtes portant sur /media (et en dessous dans l'arborescence) ne soient plus redirigées vers l'application web :
Alias /media /chemin/vers/les/medias
<Location /media>
ProxyPass !
</Location>
<Directory /chemin/vers/les/medias>
Require all granted
</Directory>
On recharge la conf' avec systemctl apache2 reload
, et hop, ça fonctionne :
$ curl -s -o /dev/null -D - -r 1315624-1562279 https://monorganisation.example/media/videos/ef2218bee799524931d2af11aafc3933e5d7413da2989b70fc94a9941fcd50e1/666/720p.ts
HTTP/1.1 206 Partial Content
Date: Wed, 30 Sep 2020 17:19:31 GMT
Server: Apache/2.4.25 (Debian)
Last-Modified: Wed, 16 Sep 2020 14:19:04 GMT
ETag: "1025278-5af6ef32e144b"
Accept-Ranges: bytes
Content-Length: 246656
Cache-Control: max-age=0, no-store
Content-Range: bytes 1315624-1562279/16929400
Content-Type: video/MP2T
Pour être exhaustif, /media est routé par Django uniquement si la variable de configuration « DEBUG » a la valeur « True ». C'est effectivement sa valeur par défaut dans le fichier settings.py. Nous ne la surchargeons pas dans le fichier settings_local.py. Il s'agit d'une erreur, désormais corrigée.
Au final, on a migré vers nginx afin d'être dans les pré-requis de l'application, ce qui permet de demander de l'aide plus facilement. De plus, une fonctionnalité de ce logiciel nécessite impérativement nginx (Apache httpd n'a pas d'équivalent), donc autant harmoniser la plateforme en installant nginx partout.
Je retiens assez peu d'éléments techniques. Je retiens surtout un défaut récurrent de notre organisation de travail. Notre équipe d'adminsys et notre équipe de devs auraient dû travailler ensemble sur ce point plutôt que de se renvoyer la balle "c'est ton périmètre, démerde-toi". Au final, c'est ce qu'on a fait car j'ai mis mon nez dedans, mais on a perdu du temps et de l'énergie (tenter de comprendre, modifier le code de l'application pour diviser les vidéos en fragments, ré-encoder les vidéos existantes, comprendre, retirer les patchs du code, ré-encoder les vidéos afin de virer les fragments, etc.). Après ça, la méta-équipe qui regroupe les deux sus-citées peut bien se nommer « devops » et notre DSI peut bien pavoiser partout qu'on est devops et faire des causeries sur le sujet. On n'est même pas au premier stade de la collaboration, celle, élémentaire, qui existe depuis bien avant le bullshit devops. Bien sûr, on me répondra que c'est facile de dégainer sur un seul exemple, ça veut rien dire, ce n'est pas représentatif, tout ça. Mais quand je dégaine plusieurs exemples, on me rétorque que je suis méchant, que j'accable les personnes. :))))