Files
ObsiGate/docs/features/media-viewers-109.md
T
bruno 80852374a8
CI / lint (push) Successful in 2m0s
CI / security (push) Successful in 1m22s
CI / test (push) Successful in 4m36s
CI / build (push) Successful in 1m18s
CI / e2e (push) Successful in 13m7s
fix: positionnement lecteur media (mobile/desktop) et drag libre de la mini-video #110
2026-09-23 11:42:23 -04:00

9.5 KiB
Raw Permalink Blame History

#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_EXTENSIONS et VIDEO_EXTENSIONS sont intégrées à SUPPORTED_EXTENSIONS (indexer.py). La branche is_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 filtre ext: suivent automatiquement.
  • Streaming : le helper Range de pdf/stream a été extrait en _stream_file_with_range(file_path, request, media_type) (206 + Content-Range + Accept-Ranges, 416 sur plage invalide, FileResponse simple sinon, lectures offloadées via asyncio.to_thread). pdf/stream l'utilise désormais aussi (comportement inchangé, tests de régression).
  • Nouvel endpoint GET /api/media/{vault}?path=… : sert audio/vidéo avec le MIME media_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, et OBSIGATE_MEDIA_MAX_INLINE_MB (défaut 500 Mo) — au-delà, l'endpoint renvoie 413 et la vue fichier bascule sur l'UI de téléchargement.
  • api_file_view() renvoie, avant tout read_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.js dispatche data.is_audio → renderAudioViewer() et data.is_video → renderVideoViewer().
    • Audio : <audio controls preload="metadata"> pleine largeur, artwork placeholder (icône Lucide audio-lines), durée lue via loadedmetadata, 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.
  • renderMediaFallback() : sur l'événement error de 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 + message viewer.media_unsupported / viewer.media_too_large + Télécharger / Ouvrir dans un nouvel onglet.
  • Pause au changement de vue : _mediaViewerCleanup met en pause et détache la source quand renderFile() 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 via load_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êtes Range étaient déjà exclues.

F. Tests

  • tests/test_media_stream.py : 206 + Content-Range + 1024 octets, 416 hors borne, 200 + Accept-Ranges sans Range, suffix range, 413 au-delà de la limite, 403 path traversal, 403 vault sans accès, 400 non-média, MIME .mov, régression pdf/stream.
  • tests/test_media_indexing.py : .mp3/.mp4/.flac dans l'arborescence et l'index, content == "", watcher pertinent, filtre ext: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éponse 206 sur requête Range. Fixtures : test_vault/sample-audio.mp3 (sine 1 s) et test_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 par renderAudioViewer / renderVideoViewer de viewer.js) ; et
  • le dock global — un enfant direct de <body> (#now-playing-host), monté hors de .content-wrapper pour 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 librement depuis n'importe quel point de la fenêtre (position absolue mémorisée, centre autorisé — aucune aimantation aux bords) 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=cover ajouté, et body.np-active ajoute le décalage du contenu. La barre audio passe en grille (progression sur sa propre ligne) et masque les actions secondaires pour éviter tout chevauchement.

Comportements

  • Naviguer (onglet, panneau, dashboard, split) ne coupe pas la lecture ; le dock apparaît.
  • Revenir au média : NowPlaying.focus() rouvre/focalise l'onglet vault::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.continues le 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-dim non définies), index.html (viewport-fit=cover), locales FR/EN (player.*).

Correctifs annexes

  • Les variables CSS --surface1 et --text-dim, utilisées mais jamais définies depuis #108/#109, sont remplacées par --surface et --text-secondary (+ --text-dim dans 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).