9.2 KiB
#109 — Support audio & vidéo (lecteurs HTML5 intégrés)
Version livrée : 2.18.0 · Statut : ✅ · Impact : 🟡 Zone : backend (
main,indexer,media_types,bookslm) + frontend (viewer.js,utils.js,style.css,sw.js).
Contexte
Le socle média de #108 (backend/media_types.py) exposait déjà
AUDIO_EXTENSIONS / VIDEO_EXTENSIONS, mais ces fichiers n'étaient ni indexés
ni affichables : ils tombaient dans le chemin binaire « Ce fichier est binaire
et ne peut pas être affiché » + bouton download.
Ce qui a été livré
A. Backend
- Indexation :
AUDIO_EXTENSIONSetVIDEO_EXTENSIONSsont intégrées àSUPPORTED_EXTENSIONS(indexer.py). La brancheis_media(ext)existante indexe nom / taille / mtime sans jamais lire les octets (content: ""), donc le TF-IDF et les logs restent propres. Le watcher et le filtreext:suivent automatiquement. - Streaming : le helper Range de
pdf/streama été extrait en_stream_file_with_range(file_path, request, media_type)(206 +Content-Range+Accept-Ranges,416sur plage invalide,FileResponsesimple sinon, lectures offloadées viaasyncio.to_thread).pdf/streaml'utilise désormais aussi (comportement inchangé, tests de régression). - Nouvel endpoint
GET /api/media/{vault}?path=…: sert audio/vidéo avec le MIMEmedia_types.media_mime_type(surcharges.m4a→audio/mp4,.opus/.oga→audio/ogg,.mov→video/quicktime,.m4v→video/mp4). - Gardes-fous :
_resolve_safe_path,check_vault_access, etOBSIGATE_MEDIA_MAX_INLINE_MB(défaut 500 Mo) — au-delà, l'endpoint renvoie413et la vue fichier bascule sur l'UI de téléchargement. api_file_view()renvoie, avant toutread_text(),is_audio/is_video/stream_url/media_mime/size_bytes. Au-delà de la limite :unsupported: true+media_too_large: true.
B/C. Frontend — lecteurs
viewer.jsdispatchedata.is_audio→renderAudioViewer()etdata.is_video→renderVideoViewer().- Audio :
<audio controls preload="metadata">pleine largeur, artwork placeholder (icône Lucideaudio-lines), durée lue vialoadedmetadata, toolbar (titre, voûte + taille, badge durée, ouvrir l'original, télécharger). - Vidéo :
<video controls playsinline preload="metadata">centrée sur une scène noire letterboxée (max-height: calc(100vh - 180px)), même toolbar avec durée + résolution.
- Audio :
renderMediaFallback(): sur l'événementerrorde l'élément média (codec hors web-natif :.mkv,.avi, HEVC…) ou si le fichier est trop volumineux, remplace le lecteur par l'UI binaire + messageviewer.media_unsupported/viewer.media_too_large+ Télécharger / Ouvrir dans un nouvel onglet.- Pause au changement de vue :
_mediaViewerCleanupmet en pause et détache la source quandrenderFile()re-rend la zone (changement d'onglet, navigation) — pas de lecture persistante en v1 (cohérence #75). EXT_ICONS(utils.js) : audio →audio-lines, vidéo →video.- i18n FR/EN (
viewer.media_unsupported,viewer.media_too_large).
D. Recherche & intégrations
- Filtres
ext:mp3,ext:mp4, etc. opérationnels (les médias entrent dans l'index). - Récents / dashboards : previews vides (aucun texte extrait) — comportement naturel de la branche binaire.
- BooksLM :
_file_entry()ignore désormais tout média (is_media) — les octets ne sont jamais envoyés au modèle ; les images restent gérées à part viaload_vault_image_data_url(vision). - Hors scope v1 (porte notée) : transcription audio via Whisper.
E. Mobile & PWA
- Le service worker ne met jamais en cache le flux média
(
/api/media/{vault}), tout en conservant le cache des miniatures (/api/media/{vault}/thumb) et la stratégie Network First pour le reste. Les requêtesRangeétaient déjà exclues.
F. Tests
tests/test_media_stream.py: 206 +Content-Range+ 1024 octets,416hors borne,200+Accept-Rangessans Range, suffix range,413au-delà de la limite,403path traversal,403vault sans accès,400non-média, MIME.mov, régressionpdf/stream.tests/test_media_indexing.py:.mp3/.mp4/.flacdans l'arborescence et l'index,content == "", watcher pertinent, filtreext:mp3.tests/frontend/media-viewer.test.mjs: helpers purs (formatMediaDuration,buildMediaUrl) + vérifications statiques (dispatch,<audio>/<video>, fallback, CSS, icônes, i18n, service worker, endpoints backend).tests/e2e/media-viewer.spec.js: lecture<audio>et<video>via/api/media+ réponse206sur requêteRange. Fixtures :test_vault/sample-audio.mp3(sine 1 s) ettest_vault/sample-video.webm(VP8 64×64).
Limitations connues
- Formats hors web-natifs (
.mkv,.avi, HEVC, AC-4) : non lisibles sans transcodage (ffmpeg hors scope) → repli téléchargement / lecteur de l'OS. - HLS, sous-titres
<track src=".vtt">et vignettes vidéo : hors scope v1. - Gros médias (> 500 Mo par défaut) : pas de lecture intégrée (protège le worker
uvicorn unique) ; ajustable via
OBSIGATE_MEDIA_MAX_INLINE_MB.
#110 — Lecteur média persistant « Now Playing »
Version livrée : 2.19.0 · Statut : ✅ · Impact : 🟡 Zone : frontend (
now-playing.js,viewer.js,ui.js,pane-manager.js,app.js,style.css,index.html, locales).
Principe : un seul média, téléporté
frontend/js/now-playing.js est un contrôleur singleton qui possède l'unique
élément <audio>/<video> de l'application. Il est déplacé par appendChild
(sans recréation, donc sans couper la lecture) entre :
- la vue inline — l'onglet/panneau du média, via la surface
NowPlaying.attachInline(area, data)(appelée parrenderAudioViewer/renderVideoViewerdeviewer.js) ; et - le dock global — un enfant direct de
<body>(#now-playing-host), monté hors de.content-wrapperpour survivre àrenderFile(), aux onglets, aux panneaux et à la reconstruction de la grille split.
renderFile() appelle NowPlaying.handleRender(area, data) : si la zone qui va
être réécrite contient l'élément média, celui-ci est renvoyé au dock. Les autres
points qui vident le contenu (dashboard _showDashboard, showWelcome,
PaneManager._buildGrid / _collapseToSingle) appellent le même hook via le
global window.NowPlaying.
Surfaces et ergonomie
- Dock audio (desktop) : pilule flottante verre dépoli centrée en bas
(
.np-dock--audio) — artwork, titre, voûte, durée, play/pause, précédent/suivant, barre de progression (seek), volume, revenir au média, agrandir, fermer. - Panneau étendu : carte centrale (bottom-sheet sur mobile) avec artwork, scrub large, volume, vitesse 0,5–2×, précédent/suivant et actions ouvrir/télécharger/fermer.
- Mini-vidéo flottante (
.np-dock--video) : déplaçable (aimantation au coin le plus proche) et redimensionnable, géométrie persistée ; sur mobile elle se fixe au-dessus de la barre d'outils. - Mobile : mini-player au-dessus de la barre 64 px
(
bottom: calc(64px + env(safe-area-inset-bottom))),viewport-fit=coverajouté, etbody.np-activeajoute le décalage du contenu.
Comportements
- Naviguer (onglet, panneau, dashboard, split) ne coupe pas la lecture ; le dock apparaît.
- Revenir au média :
NowPlaying.focus()rouvre/focalise l'ongletvault::path(window.getActiveTabManager().open). - Fermer :
NowPlaying.stop()met en pause, libère l'élément et masque le dock. - Fermer l'onglet du média en cours : la lecture continue et un toast
player.continuesle signale. - Media Session : métadonnées (
MediaMetadata) + actions play/pause/stop/seek/nexttrack/previoustrack → écran verrouillé, casque Bluetooth, touches média, Windows SMTC (WebView2). - Picture-in-Picture natif pour la vidéo (
requestPictureInPicture), bouton masqué si non supporté. - Reprise après rechargement : état (fichier, position, pause, préférences
volume/vitesse) persisté en
localStorage(obsigate-now-playing,obsigate-player-prefs,obsigate-player-pos).
Fichiers modifiés / ajoutés
- Nouveau :
frontend/js/now-playing.js(contrôleur, dock, session, PiP, persistance),tests/e2e/media-viewer.spec.js(dock/retour/fermeture/mini-vidéo). - Modifiés :
viewer.js(délégation inline +handleRender),ui.js(dashboard + toast de fermeture d'onglet),pane-manager.js(grille/collapse),app.js(initNowPlaying),style.css(dock/étendu/vidéo/mobile, et correction des variables--surface1/--text-dimnon définies),index.html(viewport-fit=cover), locales FR/EN (player.*).
Correctifs annexes
- Les variables CSS
--surface1et--text-dim, utilisées mais jamais définies depuis #108/#109, sont remplacées par--surfaceet--text-secondary(+--text-dimdans toute la feuille).
Hors scope (porte notée)
- Fenêtre vidéo détachée native Tauri (
WebviewWindowBuilder+always_on_top) : non implémentée, à faire dans une itération dédiée (Rust, capabilities, route/player).