mise à jours HEVC pour 1080p
This commit is contained in:
Regular → Executable
+133
-14
@@ -10,6 +10,7 @@ import sys
|
||||
import markdown2
|
||||
import numpy as np
|
||||
import html
|
||||
from urllib.parse import quote
|
||||
|
||||
# --- 1. CONFIGURATION ---
|
||||
base_dir = sys.argv[1] if len(sys.argv) > 1 else "./"
|
||||
@@ -27,6 +28,32 @@ if not os.path.exists(html_output_dir):
|
||||
COULEURS_JOURS = ['#FF5733', '#2ECC71', '#3498DB', '#9B59B6', '#F1C40F', '#E67E22', '#1ABC9C', '#34495E']
|
||||
noms_colonnes = ['Fichier', 'Date_Heure', 'Latitude', 'Longitude', 'Altitude', 'Vitesse']
|
||||
|
||||
# --- ÉCHAPPEMENT ---
|
||||
# Sans ça, un fichier nommé « col & vue.jpg » produit un lien cassé et du HTML
|
||||
# invalide. quote() encode l'espace et l'esperluette pour les URL, escape()
|
||||
# protège le texte affiché.
|
||||
def est_sidecar(nom):
|
||||
"""Fichiers parasites macOS.
|
||||
|
||||
Sur un disque non-HFS (exFAT d'un disque externe, cle USB, partage reseau),
|
||||
macOS depose un fichier AppleDouble « ._nom » a cote de chaque fichier pour
|
||||
y stocker les metadonnees. « ._Ete_2026-08-01.gpx » n'est donc pas un GPX
|
||||
mais un blob binaire, et gpxpy echoue dessus avec une erreur de decodage.
|
||||
On ecarte aussi .DS_Store et les fichiers caches.
|
||||
"""
|
||||
return nom.startswith('._') or nom.startswith('.')
|
||||
|
||||
|
||||
def url(chemin):
|
||||
"""Chemin sûr pour un attribut src/href."""
|
||||
return html.escape(quote(chemin))
|
||||
|
||||
|
||||
def txt(valeur):
|
||||
"""Texte sûr pour l'affichage."""
|
||||
return html.escape(str(valeur))
|
||||
|
||||
|
||||
# --- FONCTIONS UTILES ---
|
||||
def nettoyer_et_trier(df):
|
||||
if df is None or df.empty:
|
||||
@@ -52,11 +79,44 @@ def create_pin(color, icon_name):
|
||||
def inject_common_assets(m):
|
||||
custom_js = """
|
||||
<script>
|
||||
// --- DÉTECTION HEVC ---
|
||||
// Stratégie hybride : le 720p (H.264) est chargé par défaut et se lit
|
||||
// partout. Seul le 1080p est en HEVC. Si le navigateur ne sait pas décoder
|
||||
// le HEVC (Firefox principalement), on neutralise le bouton 1080p —
|
||||
// la vidéo reste lisible, seule l'option haute qualité disparaît.
|
||||
var HEVC_OK = (function () {
|
||||
try {
|
||||
var v = document.createElement('video');
|
||||
var t = v.canPlayType('video/mp4; codecs="hvc1.1.6.L120.B0"');
|
||||
return t === 'probably' || t === 'maybe';
|
||||
} catch (e) { return false; }
|
||||
})();
|
||||
|
||||
function applyCodecSupport(popup) {
|
||||
if (HEVC_OK) return;
|
||||
var btns = popup.querySelectorAll('.quality-btn[data-hevc="1"]');
|
||||
for (var i = 0; i < btns.length; i++) {
|
||||
btns[i].classList.add('qbtn-disabled');
|
||||
btns[i].title = "Le 1080p est encodé en HEVC, que ce navigateur ne sait pas décoder. Essaie Safari, Chrome ou Edge.";
|
||||
}
|
||||
var note = popup.querySelector('.hevc-note');
|
||||
if (note) note.style.display = 'block';
|
||||
}
|
||||
|
||||
// --- GESTION DES BOUTONS 720p / 1080p ---
|
||||
function changeQuality(videoId, newSrc, btnElement) {
|
||||
if (btnElement.classList.contains('qbtn-disabled')) return;
|
||||
|
||||
var video = document.getElementById(videoId);
|
||||
|
||||
if (video.src.includes(newSrc)) return;
|
||||
|
||||
// On compare les NOMS DE FICHIER, pas les chemins : video.src est une
|
||||
// URL absolue résolue par le navigateur, alors que newSrc est relatif
|
||||
// ("../videos/X.mp4"). L'ancien test `video.src.includes(newSrc)` ne
|
||||
// pouvait donc jamais être vrai, et recliquer sur la qualité déjà
|
||||
// active rechargeait la vidéo en repartant du début.
|
||||
var nomActuel = decodeURIComponent(video.src).split('/').pop();
|
||||
var nomVoulu = decodeURIComponent(newSrc).split('/').pop();
|
||||
if (nomActuel === nomVoulu) return;
|
||||
|
||||
var savedTime = video.currentTime;
|
||||
var wasPlaying = !video.paused;
|
||||
@@ -99,8 +159,15 @@ def inject_common_assets(m):
|
||||
mutation.addedNodes.forEach(function(node) {
|
||||
// Si l'élément ajouté est une popup Leaflet
|
||||
if (node.nodeType === 1 && node.classList && node.classList.contains('leaflet-popup')) {
|
||||
applyCodecSupport(node);
|
||||
var video = node.querySelector('video');
|
||||
if (video) {
|
||||
// Filet de sécurité : si le 1080p HEVC échoue malgré la
|
||||
// détection, on retombe automatiquement sur le 720p H.264.
|
||||
video.addEventListener('error', function () {
|
||||
var fb = this.getAttribute('data-fallback');
|
||||
if (fb && this.src.indexOf(fb) === -1) { this.src = fb; this.load(); }
|
||||
});
|
||||
console.log("🎬 Popup ouverte : Lancement de la vidéo...");
|
||||
var playPromise = video.play();
|
||||
if (playPromise !== undefined) {
|
||||
@@ -170,6 +237,17 @@ def inject_common_assets(m):
|
||||
.active-720 { background-color: #2ECC71 !important; color: white; border-color: #27ae60 !important; opacity: 1 !important; }
|
||||
.active-1080 { background-color: #007AFF !important; color: white; border-color: #0056b3 !important; opacity: 1 !important; }
|
||||
|
||||
.qbtn-disabled {
|
||||
opacity: 0.35 !important; cursor: not-allowed !important;
|
||||
text-decoration: line-through; background: #eee !important;
|
||||
}
|
||||
.qbtn-disabled:hover { opacity: 0.35 !important; background: #eee !important; }
|
||||
.hevc-note {
|
||||
display: none; margin-top: 6px; font-family: sans-serif; font-size: 11px;
|
||||
color: #8a6d3b; background: #fcf8e3; border-radius: 6px; padding: 6px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.leaflet-top.leaflet-left { top: auto !important; bottom: 20px !important; left: 15px !important; }
|
||||
.leaflet-popup-content-wrapper { border-radius: 12px; max-width: 95vw !important; }
|
||||
#journal-panel {
|
||||
@@ -211,6 +289,8 @@ try:
|
||||
jours_gpx = set()
|
||||
if os.path.exists(routes_dir):
|
||||
for f in os.listdir(routes_dir):
|
||||
if est_sidecar(f):
|
||||
continue
|
||||
match = re.search(r'(\d{4}-\d{2}-\d{2})', f)
|
||||
if match: jours_gpx.add(pd.to_datetime(match.group(1)).date())
|
||||
tous_les_jours = sorted(list(set(df_v['Jour'].unique()) | set(df_p['Jour'].unique()) | jours_gpx))
|
||||
@@ -240,12 +320,18 @@ for i, jour in enumerate(tous_les_jours):
|
||||
|
||||
marker_cluster = MarkerCluster(name="Médias").add_to(m_day)
|
||||
day_coords = []
|
||||
media_coords = [] # position des photos/vidéos, filet de cadrage
|
||||
current_gpx_filename = None
|
||||
|
||||
if os.path.exists(routes_dir):
|
||||
for gpx_file in os.listdir(routes_dir):
|
||||
if gpx_file.startswith(day_str) and gpx_file.endswith('.gpx'):
|
||||
current_gpx_filename = gpx_file
|
||||
# `day_str in gpx_file` et non `.startswith(day_str)` : la liste des
|
||||
# jours est bâtie avec re.search (date N'IMPORTE OÙ dans le nom),
|
||||
# donc une trace « trace_2026-07-15_col.gpx » créait bien un jour
|
||||
# mais n'était jamais retrouvée ici — jour vide, carte non cadrée.
|
||||
if est_sidecar(gpx_file):
|
||||
continue
|
||||
if day_str in gpx_file and gpx_file.lower().endswith('.gpx'):
|
||||
try:
|
||||
with open(os.path.join(routes_dir, gpx_file), 'r') as f:
|
||||
gpx = gpxpy.parse(f)
|
||||
@@ -259,12 +345,20 @@ for i, jour in enumerate(tous_les_jours):
|
||||
folium.PolyLine(pts, color=color_day, weight=7, opacity=0.8, popup=folium.Popup(popup_html, max_width=200)).add_to(m_global)
|
||||
folium.PolyLine(pts, color=color_day, weight=3, opacity=0.8).add_to(m_mini)
|
||||
folium.PolyLine(pts, color="#FF0000", weight=4, opacity=0.8).add_to(m_day)
|
||||
except: pass
|
||||
# Le nom n'est retenu qu'APRES analyse reussie : sinon le
|
||||
# bouton de telechargement pouvait pointer sur un fichier
|
||||
# illisible traite apres la vraie trace.
|
||||
current_gpx_filename = gpx_file
|
||||
except Exception as e:
|
||||
# `except: pass` avalait TOUT, y compris un Ctrl-C, et
|
||||
# masquait les GPX corrompus : la trace disparaissait sans
|
||||
# le moindre message.
|
||||
print(f" ⚠️ GPX illisible ({gpx_file}) : {e}")
|
||||
|
||||
m_day.get_root().html.add_child(folium.Element('<div style="position: fixed; top: 15px; left: 15px; z-index: 10001;"><a href="../index.html" target="_top" style="text-decoration: none; background: #333; color: white; padding: 12px 20px; border-radius: 10px; font-family: sans-serif; font-weight: bold; box-shadow: 0 4px 10px rgba(0,0,0,0.5); border: 2px solid white; display: block;">🏠 Accueil</a></div>' + get_journal_ui()))
|
||||
|
||||
if current_gpx_filename:
|
||||
m_day.get_root().html.add_child(folium.Element(f'<div style="position: fixed; bottom: 85px; right: 15px; z-index: 10001;"><a href="../routes/{current_gpx_filename}" download style="text-decoration: none; background: #28a745; color: white; padding: 12px 20px; border-radius: 50px; font-family: sans-serif; font-weight: bold; box-shadow: 0 4px 15px rgba(0,0,0,0.4); border: 2px solid white; display: block;">📥 GPX</a></div>'))
|
||||
m_day.get_root().html.add_child(folium.Element(f'<div style="position: fixed; bottom: 85px; right: 15px; z-index: 10001;"><a href="../routes/{url(current_gpx_filename)}" download style="text-decoration: none; background: #28a745; color: white; padding: 12px 20px; border-radius: 50px; font-family: sans-serif; font-weight: bold; box-shadow: 0 4px 15px rgba(0,0,0,0.4); border: 2px solid white; display: block;">📥 GPX</a></div>'))
|
||||
|
||||
# REGROUPEMENT PHOTOS
|
||||
day_p = df_p[df_p['Jour'] == jour].copy()
|
||||
@@ -276,8 +370,14 @@ for i, jour in enumerate(tous_les_jours):
|
||||
groupe = day_p[masque]
|
||||
fichiers = groupe['Fichier'].tolist()
|
||||
nb = len(fichiers)
|
||||
slides = "".join([f'<div class="slide" style="display:{"block" if idx == 0 else "none"};"><div class="file-header">{p.strip()}</div><a href="../photos/{p.strip()}" target="_blank"><img src="../photos/{p.strip()}" loading="lazy"></a></div>' for idx, p in enumerate(fichiers)])
|
||||
slides = "".join([
|
||||
f'<div class="slide" style="display:{"block" if idx == 0 else "none"};">'
|
||||
f'<div class="file-header">{txt(p.strip())}</div>'
|
||||
f'<a href="../photos/{url(p.strip())}" target="_blank">'
|
||||
f'<img src="../photos/{url(p.strip())}" alt="{txt(p.strip())}" loading="lazy"></a></div>'
|
||||
for idx, p in enumerate(fichiers)])
|
||||
btns = f'<button class="nav-btn prev" onclick="moveSlide(this, -1)">❮</button><button class="nav-btn next" onclick="moveSlide(this, 1)">❯</button><div class="slide-counter">1/{nb}</div>' if nb > 1 else ""
|
||||
media_coords.append([ref['Latitude'], ref['Longitude']])
|
||||
folium.Marker(location=[ref['Latitude'], ref['Longitude']], popup=folium.Popup(f'<div class="slider-container"><div class="slides">{slides}</div>{btns}</div>', max_width="100%"), icon=create_pin("#FF3B30", "camera" if nb==1 else "images")).add_to(marker_cluster)
|
||||
day_p = day_p[~masque]
|
||||
|
||||
@@ -291,8 +391,9 @@ for i, jour in enumerate(tous_les_jours):
|
||||
clean_id = re.sub(r'[^a-zA-Z0-9]', '', base_name)
|
||||
video_id = f"vid_{clean_id}"
|
||||
|
||||
desktop_file = f"../videos/{base_name}.mp4"
|
||||
mobile_file = f"../videos/{base_name}_mobile.mp4"
|
||||
desktop_file = f"../videos/{url(base_name)}.mp4"
|
||||
mobile_file = f"../videos/{url(base_name)}_mobile.mp4"
|
||||
media_coords.append(pts_v[0])
|
||||
|
||||
# Ajout 'autoplay muted' pour le lancement au clic
|
||||
# Ajout 'preload=none' pour ne pas charger les autres
|
||||
@@ -301,11 +402,13 @@ for i, jour in enumerate(tous_les_jours):
|
||||
f'<div style="width:calc(85vw - 40px); max-width:1000px; text-align:center;">'
|
||||
|
||||
f'<div class="header-row">'
|
||||
f'<span class="file-name" title="{v_name.strip()}">{v_name.strip()}</span>'
|
||||
f'<span class="file-name" title="{txt(v_name.strip())}">{txt(v_name.strip())}</span>'
|
||||
f'<div class="quality-controls">'
|
||||
# Le bouton 720p est actif par défaut (cohérent avec le fichier chargé)
|
||||
f'<span class="quality-btn active-720" onclick="changeQuality(\'{video_id}\', \'{mobile_file}\', this)">720p</span>'
|
||||
f'<span class="quality-btn" onclick="changeQuality(\'{video_id}\', \'{desktop_file}\', this)">1080p</span>'
|
||||
# data-hevc : ce palier est en H.265, il sera grisé si le
|
||||
# navigateur ne sait pas le décoder.
|
||||
f'<span class="quality-btn" data-hevc="1" onclick="changeQuality(\'{video_id}\', \'{desktop_file}\', this)">1080p</span>'
|
||||
f'</div>'
|
||||
f'</div>'
|
||||
|
||||
@@ -314,12 +417,28 @@ for i, jour in enumerate(tous_les_jours):
|
||||
# 3. Pas d'autoplay.
|
||||
f'<video id="{video_id}" style="width:100%; max-height:70vh; border-radius:8px; background:black;" '
|
||||
f'controls playsinline webkit-playsinline preload="none" '
|
||||
f'src="{mobile_file}">' # <-- On pointe directement sur le fichier léger
|
||||
f'</video></div>'
|
||||
f'src="{mobile_file}" data-fallback="{mobile_file}">' # <-- 720p H.264 : lisible partout
|
||||
f'</video>'
|
||||
|
||||
# Note discrète affichée uniquement si le 1080p est indisponible
|
||||
f'<div class="hevc-note">'
|
||||
f'Le 1080p est encodé en H.265/HEVC, que ce navigateur ne sait pas décoder. '
|
||||
f'La lecture en 720p fonctionne normalement ; pour la haute définition, '
|
||||
f'utilise Safari, Chrome ou Edge.'
|
||||
f'</div>'
|
||||
|
||||
f'</div>'
|
||||
)
|
||||
folium.Marker(location=pts_v[0], popup=folium.Popup(v_pop, max_width="100%"), icon=create_pin("#007AFF", "play")).add_to(marker_cluster)
|
||||
|
||||
if day_coords: m_day.fit_bounds(day_coords, padding=(50, 50))
|
||||
# Cadrage : la trace GPX en priorité, sinon la position des médias.
|
||||
# Sans ce repli, un jour sans GPX (mais avec photos) s'ouvrait sur une
|
||||
# mappemonde centrée au large de l'Afrique.
|
||||
cadrage = day_coords if day_coords else media_coords
|
||||
if cadrage:
|
||||
m_day.fit_bounds(cadrage, padding=(50, 50))
|
||||
all_global_coords.extend(media_coords)
|
||||
|
||||
folium.LayerControl(position='topright').add_to(m_day)
|
||||
plugins.Fullscreen(position='topright').add_to(m_day)
|
||||
m_day.save(os.path.join(html_output_dir, file_name))
|
||||
|
||||
Reference in New Issue
Block a user