Bonjour la communauté,
Je recherche depuis un moment un moyen d’avoir avec jeedom un affichage des flux rtsp sans latence (car le retard de plusieurs seconde je trouve ca frustrant) et fluide sans être gourmand en cpu non plus (car avec le camera hd on est souvent obligé de convertir en x.264 en plus). comme le fait un nvr ou une application native de camera, elle ne stocke pas et ne converti pas le video.
j’ai fait un essai en local en installant mediamtx mais il diffuse le flux en webrtc de coup je suis obligé de faire une conf apache via proxy/rewrite/… :
# === Reverse proxy MediaMTX (WebRTC/HLS) ===
ProxyRequests Off
ProxyPreserveHost On
RewriteEngine On
# Redirige /webrtc/<cam> → /webrtc/<cam>/
RewriteCond %{REQUEST_URI} ^/webrtc/[^/]+$ [NC]
RewriteRule ^/webrtc/([^/]+)$ /webrtc/$1/ [R=301,L]
# WebSocket / HTTP
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule ^/webrtc/(.*)$ ws://127.0.0.1:8889/$1 [P,L]
RewriteRule ^/webrtc/(.*)$ http://127.0.0.1:8889/$1 [P,L]
ProxyPassReverse /webrtc/ http://127.0.0.1:8889/
<Location /webrtc/>
Require all granted
</Location>
j’obitient à un résultat hyper fluide avec une charge CPU à 6% même avec 10 camera en //, le flux est en temps réel y a plus d’histoire de playlist m3u8, j’utilise une page php pour afficher soit les video sous forme de liste récupéré dans la conf mediamtx, soit via un iframe et la je passe en parametre de l’url le nom de la camera je n’ai pas le flux video :
<?php
// webrtc_player_dynamic.php
// - Mode normal: découverte MediaMTX + UI complète
// - Mode iframe (si ?path=xxx): pas de header, pas de sélecteur, juste la vidéo
$API_BASE = getenv('MTX_API') ?: 'http://127.0.0.1:8889';
$embedPath = isset($_GET['path']) ? trim($_GET['path']) : '';
// ---------- fonctions utilitaires ----------
function fetch_json($url, $timeout = 2) {
$ctx = stream_context_create(['http' => ['timeout' => $timeout]]);
$raw = @file_get_contents($url, false, $ctx);
if ($raw === false) return [null, 0, error_get_last()['message'] ?? 'unknown'];
$code = 0;
if (!empty($http_response_header)) {
foreach ($http_response_header as $h) {
if (preg_match('~^HTTP/\S+\s+(\d{3})~i', $h, $m)) { $code = intval($m[1]); break; }
}
}
return [json_decode($raw, true), $code, null];
}
// ---------- découverte des paths (mode normal uniquement) ----------
$paths = [];
$err = null;
if ($embedPath === '') {
list($j, $code, $e) = fetch_json($API_BASE . '/v3/paths/list');
if ($j && isset($j['items']) && is_array($j['items'])) {
foreach ($j['items'] as $it) if (isset($it['name'])) $paths[] = $it['name'];
} else {
list($j2, $code2, $e2) = fetch_json($API_BASE . '/v2/paths/list');
if ($j2 && isset($j2['items']) && is_array($j2['items'])) {
foreach ($j2['items'] as $it) if (isset($it['name'])) $paths[] = $it['name'];
} else {
$err = "Impossible d'interroger l'API MediaMTX ($API_BASE): " . htmlspecialchars(($e2 ?: $e) . " HTTP:$code/$code2");
}
}
if (!$paths) {
$yml = @file('/etc/mediamtx.yml');
if ($yml) {
$inPaths = false;
foreach ($yml as $line) {
if (preg_match('/^\s*paths\s*:\s*$/', $line)) { $inPaths = true; continue; }
if ($inPaths) {
if (preg_match('/^\S/', $line)) break;
if (preg_match('/^\s{2,}([a-zA-Z0-9._-]+)\s*:\s*$/', $line, $m)) $paths[] = $m[1];
}
}
if ($paths) $err = null;
}
}
sort($paths);
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title><?php echo $embedPath ? 'Player' : 'SOFTECH'; ?></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root{--bg:#121212;--panel:#1e1e1e;--panel2:#1f1f1f;--txt:#fff;--muted:#bfbfbf;--accent:#00bfff;--err:#ffb380}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--txt);font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
<?php if(!$embedPath): ?>
header{background:var(--panel2);padding:14px 20px;text-align:center;font-weight:700;font-size:1.25rem;color:var(--accent);box-shadow:0 2px 5px rgba(0,0,0,.3)}
main{display:flex;flex-direction:column;align-items:center;padding:28px 16px;gap:20px}
.card{background:var(--panel);padding:22px;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.45);width:100%;max-width:700px}
.row{display:flex;gap:14px;align-items:center}
.row + .row{margin-top:14px}
label{min-width:86px}
select,button{width:100%;padding:10px 12px;border:none;border-radius:8px;font-size:1rem;outline:none}
select{background:#2b2b2b;color:var(--txt)}
button{background:var(--accent);color:#fff;cursor:pointer;transition:filter .2s}
button:hover{filter:brightness(.95)}
.status{margin-top:10px;font-style:italic;color:var(--muted);text-align:center}
.status .err{color:var(--err)}
.card.video{padding:0}
<?php else: /* mode iframe: minimal, pas de marges superflues */ ?>
main{padding:0}
<?php endif; ?>
video{display:block;max-width:100%;width:100%;height:auto;border:0;border-radius:12px;background:#000;box-shadow:0 2px 10px rgba(0,0,0,.5)}
</style>
</head>
<body>
<?php if(!$embedPath): /* --------- MODE NORMAL --------- */ ?>
<header>PLAYER</header>
<main>
<div class="card">
<div class="row">
<label for="cam">Caméra :</label>
<select id="cam">
<?php if ($paths): foreach ($paths as $p): ?>
<option value="<?= htmlspecialchars($p) ?>"><?= htmlspecialchars($p) ?></option>
<?php endforeach; else: ?>
<option value="" disabled selected>(aucune détectée)</option>
<?php endif; ?>
</select>
</div>
<div class="row"><button id="play">▶️ Lire</button></div>
<div class="status" id="status"><?= $err ? '<span class="err">'. $err .'</span>' : 'Prêt' ?></div>
</div>
<div class="card video">
<video id="video" autoplay playsinline muted controls></video>
</div>
</main>
<?php else: /* --------- MODE IFRAME (path fourni) --------- */ ?>
<main>
<video id="video" autoplay playsinline muted controls></video>
</main>
<?php endif; ?>
<script>
let pc = null;
const video = document.getElementById('video');
const setStatus = t => { const el = document.getElementById('status'); if(el) el.innerHTML = t; };
async function play(cam){
if(!cam) return;
if(pc){ try{pc.close();}catch(e){} pc=null; }
if (typeof setStatus === 'function') setStatus("Connexion...");
const peer = new RTCPeerConnection({iceServers: []});
pc = peer;
peer.addTransceiver("video",{direction:"recvonly"});
peer.addTransceiver("audio",{direction:"recvonly"});
peer.addEventListener("track", e => { video.srcObject = e.streams[0]; });
try{
const offer = await peer.createOffer();
await peer.setLocalDescription(offer);
const endpoint = `/webrtc/${encodeURIComponent(cam)}/whep`;
const r = await fetch(endpoint,{method:"POST",headers:{ "Content-Type":"application/sdp" },body:offer.sdp});
if(!r.ok) throw new Error("HTTP "+r.status);
const answer = await r.text();
await peer.setRemoteDescription({type:"answer",sdp:answer});
if (typeof setStatus === 'function') setStatus("Lecture en cours ("+cam+")");
}catch(err){
console.error(err);
if (typeof setStatus === 'function') setStatus('<span class="err">Erreur de connexion à la caméra.</span>');
try{peer.close();}catch(_){}
pc=null;
}
}
// ---- wiring mode normal
<?php if(!$embedPath): ?>
document.getElementById('play').addEventListener('click', ()=>{
const cam = document.getElementById('cam').value;
play(cam);
});
// démarrage auto si ?path=...
(function(){
const u=new URLSearchParams(location.search);
if(u.has('path')){
const p=u.get('path');
const sel=document.getElementById('cam');
const opt=[...sel.options].find(o=>o.value===p);
if(opt){ sel.value=p; play(p); }
}
})();
<?php else: ?>
// ---- mode iframe: démarrage automatique avec le path fourni
play(<?php echo json_encode($embedPath); ?>);
<?php endif; ?>
// Nettoyage
window.addEventListener('beforeunload',()=>{ if(pc){ try{pc.close();}catch(e){} pc=null; }});
</script>
</body>
</html>
et du coup je peux le mettre directement dans un widgetCustom jeedom l’iframe et j’ai la vidéos en direct , mais le problème de ma solution fonctionne que si je suis sur le réseau local, mais pas via le dns jeedom en https:
j’ai pas les compétence pour développer moi même un plugin, du coup j’aimerais savoir si cette proposition pourrait intéressé le plugin officiel camera de Jeedom, l’avantage du plugin c’est qu’il n’y aurait pas besoin de conf apache et de page php pour l’affichage non plus.
Merci d’avance de votre indulgence et prise en compte.
en espérant que ce message fera echo
Bonne journée


