Compare commits

..

2 Commits

Author SHA1 Message Date
Jon
a58177c8cd Added speedo pop up / full screen 2026-04-29 22:15:07 +01:00
Jon
7ce94f78c4 Added subtitles of info 2026-04-29 22:14:38 +01:00

View File

@@ -12,15 +12,47 @@
#rec-indicator { position: absolute; top: 12px; left: 50%; transform: translateX(-50%); background: rgba(180,0,0,0.75); border-radius: 20px; padding: 4px 12px; color: #fff; font-size: 11px; letter-spacing: 1px; display: none; align-items: center; gap: 6px; white-space: nowrap; } #rec-indicator { position: absolute; top: 12px; left: 50%; transform: translateX(-50%); background: rgba(180,0,0,0.75); border-radius: 20px; padding: 4px 12px; color: #fff; font-size: 11px; letter-spacing: 1px; display: none; align-items: center; gap: 6px; white-space: nowrap; }
#rec-dot { width: 7px; height: 7px; border-radius: 50%; background: #fff; animation: blink 1s infinite; } #rec-dot { width: 7px; height: 7px; border-radius: 50%; background: #fff; animation: blink 1s infinite; }
#rec-timer { font-size: 11px; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} } @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} }
#speed-display { position: absolute; top: 12px; right: 12px; text-align: center; background: rgba(0,0,0,0.45); border-radius: 12px; padding: 6px 14px; } /* SMALL speedo — top right */
#speed-display {
position: absolute; top: 12px; right: 12px;
text-align: center; background: rgba(0,0,0,0.45);
border-radius: 12px; padding: 6px 14px;
pointer-events: all; cursor: pointer;
transition: opacity 0.2s;
}
#speed-display:hover { background: rgba(255,255,255,0.12); }
#speed-val { font-size: 44px; font-weight: 700; color: #fff; line-height: 1; } #speed-val { font-size: 44px; font-weight: 700; color: #fff; line-height: 1; }
#speed-unit { font-size: 10px; color: #aaa; letter-spacing: 1.5px; margin-top: 1px; } #speed-unit { font-size: 10px; color: #aaa; letter-spacing: 1.5px; margin-top: 1px; }
#max-speed { font-size: 10px; color: #666; margin-top: 2px; }
#gps-bar { position: absolute; bottom: 48px; left: 0; right: 0; background: rgba(0,0,0,0.5); padding: 5px 12px; display: none; flex-direction: column; gap: 3px; } /* BIG speedo overlay */
#speedo-overlay {
position: absolute; inset: 0;
background: rgba(0,0,0,0.92);
display: none; flex-direction: column;
align-items: center; justify-content: center;
pointer-events: all; cursor: pointer;
z-index: 10;
}
#speedo-overlay.visible { display: flex; }
#speedo-close {
position: absolute; top: 12px; right: 14px;
color: rgba(255,255,255,0.5); font-size: 22px;
cursor: pointer; font-family: var(--font-sans);
pointer-events: all; line-height: 1;
background: none; border: none;
}
#speedo-close:hover { color: #fff; }
#big-speed-val { font-size: clamp(80px, 22vw, 160px); font-weight: 700; color: #fff; line-height: 1; text-align: center; }
#big-speed-unit { font-size: clamp(14px, 3vw, 22px); color: #aaa; letter-spacing: 3px; margin-top: 8px; text-align: center; }
#big-gps-info { margin-top: 28px; text-align: center; display: flex; flex-direction: column; gap: 8px; }
.big-data-row { display: flex; gap: 24px; justify-content: center; flex-wrap: wrap; }
.bdi { font-size: clamp(11px, 2vw, 15px); color: #ccc; display: flex; gap: 6px; align-items: center; }
.bdl { color: #555; font-size: clamp(9px, 1.5vw, 12px); letter-spacing: 0.5px; }
.bdv { color: #fff; font-weight: 500; }
#gps-bar { position: absolute; bottom: 48px; left: 0; right: 0; background: rgba(0,0,0,0.5); padding: 5px 12px; display: none; }
.data-row { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; } .data-row { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
.di { font-size: 10px; color: #ccc; display: flex; gap: 4px; align-items: center; } .di { font-size: 10px; color: #ccc; display: flex; gap: 4px; align-items: center; }
.dl { color: #666; font-size: 9px; letter-spacing: 0.4px; } .dl { color: #666; font-size: 9px; letter-spacing: 0.4px; }
@@ -30,10 +62,9 @@
@keyframes gpsblink { 0%,100%{opacity:1} 50%{opacity:0.4} } @keyframes gpsblink { 0%,100%{opacity:1} 50%{opacity:0.4} }
#status-bar { position: absolute; bottom: 90px; left: 12px; font-size: 10px; color: rgba(255,255,255,0.4); font-family: var(--font-sans); pointer-events: none; } #status-bar { position: absolute; bottom: 90px; left: 12px; font-size: 10px; color: rgba(255,255,255,0.4); font-family: var(--font-sans); pointer-events: none; }
#placeholder { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #888; gap: 10px; font-family: var(--font-sans); background: #000; } #placeholder { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #888; gap: 10px; font-family: var(--font-sans); background: #000; }
#btn-bar { position: absolute; bottom: 0; left: 0; right: 0; display: flex; justify-content: center; align-items: center; gap: 10px; padding: 7px 12px; background: rgba(0,0,0,0.5); pointer-events: all; flex-wrap: wrap; } #btn-bar { position: absolute; bottom: 0; left: 0; right: 0; display: flex; justify-content: center; align-items: center; gap: 10px; padding: 7px 12px; background: rgba(0,0,0,0.5); pointer-events: all; flex-wrap: wrap; z-index: 20; }
.hud-btn { background: rgba(255,255,255,0.1); border: 0.5px solid rgba(255,255,255,0.2); color: #fff; border-radius: 20px; padding: 5px 13px; font-size: 11px; font-family: var(--font-sans); cursor: pointer; letter-spacing: 0.4px; white-space: nowrap; } .hud-btn { background: rgba(255,255,255,0.1); border: 0.5px solid rgba(255,255,255,0.2); color: #fff; border-radius: 20px; padding: 5px 13px; font-size: 11px; font-family: var(--font-sans); cursor: pointer; letter-spacing: 0.4px; white-space: nowrap; }
.hud-btn:hover { background: rgba(255,255,255,0.2); } .hud-btn:hover { background: rgba(255,255,255,0.2); }
.hud-btn:disabled { opacity: 0.3; cursor: default; } .hud-btn:disabled { opacity: 0.3; cursor: default; }
@@ -64,10 +95,29 @@
<span id="rec-timer">00:00</span> <span id="rec-timer">00:00</span>
</div> </div>
<div id="speed-display"> <!-- small speedo, tappable when not recording -->
<div id="speed-display" onclick="openSpeedo()">
<div id="speed-val">--</div> <div id="speed-val">--</div>
<div id="speed-unit">MPH</div> <div id="speed-unit">MPH</div>
<div id="max-speed">MAX --</div> </div>
<!-- big speedo overlay -->
<div id="speedo-overlay" onclick="closeSpeedo()">
<button id="speedo-close" onclick="closeSpeedo()">&#x2715;</button>
<div id="big-speed-val">--</div>
<div id="big-speed-unit">MPH</div>
<div id="big-gps-info">
<div class="big-data-row">
<div class="bdi"><span class="bdl">LAT</span><span class="bdv" id="b-lat">--</span></div>
<div class="bdi"><span class="bdl">LNG</span><span class="bdv" id="b-lng">--</span></div>
</div>
<div class="big-data-row">
<div class="bdi"><span class="bdl">ALT</span><span class="bdv" id="b-alt">--</span></div>
<div class="bdi"><span class="bdl">ACC</span><span class="bdv" id="b-acc">--</span></div>
<div class="bdi"><span class="bdl">HDG</span><span class="bdv" id="b-hdg">--</span></div>
<div class="bdi"><span class="bdl">DIST</span><span class="bdv" id="b-dist">--</span></div>
</div>
</div>
</div> </div>
<div id="gps-bar"> <div id="gps-bar">
@@ -99,10 +149,14 @@ let stream = null, facingMode = 'environment';
let watchId = null, clockInterval = null; let watchId = null, clockInterval = null;
let useMph = true, maxSpeed = 0; let useMph = true, maxSpeed = 0;
let lastPos = null, totalDist = 0; let lastPos = null, totalDist = 0;
let mediaRecorder = null, recordedChunks = [], recInterval = null, recSeconds = 0; let mediaRecorder = null, recordedChunks = [];
let isRecording = false; let recInterval = null, recSeconds = 0, isRecording = false;
let speedoOpen = false;
let srtEntries = [], recStartTime = null;
let currentGPS = { lat: null, lng: null, alt: null, acc: null, hdg: null, speed: 0 };
function cvt(ms) { return useMph ? ms * 2.23694 : ms * 3.6; } function cvt(ms) { return useMph ? ms * 2.23694 : ms * 3.6; }
function cvtLabel() { return useMph ? 'mph' : 'kph'; }
function haversine(a, b) { function haversine(a, b) {
const R = 6371000, r = Math.PI / 180; const R = 6371000, r = Math.PI / 180;
@@ -116,7 +170,8 @@ function bearingLabel(deg) {
return ['N','NE','E','SE','S','SW','W','NW'][Math.round(deg/45)%8] + ' ' + Math.round(deg) + '\xb0'; return ['N','NE','E','SE','S','SW','W','NW'][Math.round(deg/45)%8] + ' ' + Math.round(deg) + '\xb0';
} }
function pad(v) { return String(v).padStart(2,'0'); } function pad(v) { return String(Math.floor(v)).padStart(2,'0'); }
function pad3(v) { return String(Math.floor(v)).padStart(3,'0'); }
function updateClock() { function updateClock() {
const n = new Date(); const n = new Date();
@@ -128,86 +183,105 @@ function updateClock() {
function setStatus(msg) { document.getElementById('status-bar').textContent = msg; } function setStatus(msg) { document.getElementById('status-bar').textContent = msg; }
function startGPS() { function openSpeedo() {
if (!navigator.geolocation) { setStatus('GPS unavailable'); return; } if (isRecording) return;
document.getElementById('gps-bar').style.display = 'flex'; speedoOpen = true;
watchId = navigator.geolocation.watchPosition(pos => { document.getElementById('speedo-overlay').classList.add('visible');
const c = pos.coords; syncBigSpeedo();
const spd = c.speed !== null && c.speed >= 0 ? c.speed : 0; }
const conv = cvt(spd);
if (conv > maxSpeed) maxSpeed = conv; function closeSpeedo() {
const cur = { lat: c.latitude, lng: c.longitude }; speedoOpen = false;
if (lastPos) { const d = haversine(lastPos, cur); if (d < 200) totalDist += d; } document.getElementById('speedo-overlay').classList.remove('visible');
lastPos = cur; }
document.getElementById('speed-val').textContent = Math.round(conv);
document.getElementById('max-speed').textContent = 'MAX ' + Math.round(maxSpeed); function syncBigSpeedo() {
document.getElementById('lat-val').textContent = c.latitude.toFixed(5); if (!speedoOpen) return;
document.getElementById('lng-val').textContent = c.longitude.toFixed(5); const unit = useMph ? 'MPH' : 'KPH';
document.getElementById('alt-val').textContent = c.altitude !== null ? Math.round(c.altitude)+'m' : '--'; document.getElementById('big-speed-val').textContent = document.getElementById('speed-val').textContent;
document.getElementById('acc-val').textContent = Math.round(c.accuracy)+'m'; document.getElementById('big-speed-unit').textContent = unit;
document.getElementById('hdg-val').textContent = bearingLabel(c.heading); document.getElementById('b-lat').textContent = document.getElementById('lat-val').textContent;
document.getElementById('dist-val').textContent = (totalDist/1000).toFixed(2)+'km'; document.getElementById('b-lng').textContent = document.getElementById('lng-val').textContent;
document.getElementById('gps-dot').className = 'active'; document.getElementById('b-alt').textContent = document.getElementById('alt-val').textContent;
setStatus(''); document.getElementById('b-acc').textContent = document.getElementById('acc-val').textContent;
}, err => { document.getElementById('b-hdg').textContent = document.getElementById('hdg-val').textContent;
setStatus('GPS: ' + err.message); document.getElementById('b-dist').textContent = document.getElementById('dist-val').textContent;
document.getElementById('gps-dot').className = ''; }
}, { enableHighAccuracy: true, maximumAge: 1000, timeout: 15000 });
function toSRTTime(ms) {
const h = Math.floor(ms/3600000), m = Math.floor((ms%3600000)/60000);
const s = Math.floor((ms%60000)/1000), mil = ms%1000;
return pad(h)+':'+pad(m)+':'+pad(s)+','+pad3(mil);
}
function captureSRTFrame() {
const now = new Date();
const elapsed = Date.now() - recStartTime;
const dateStr = now.getFullYear()+'-'+pad(now.getMonth()+1)+'-'+pad(now.getDate());
const timeStr = pad(now.getHours())+':'+pad(now.getMinutes())+':'+pad(now.getSeconds());
const spd = currentGPS.speed !== null ? Math.round(cvt(currentGPS.speed)) : '--';
srtEntries.push({
startMs: elapsed, endMs: elapsed + 1000,
lines: [
dateStr + ' ' + timeStr,
'Speed: ' + spd + ' ' + cvtLabel() + ' Hdg: ' + bearingLabel(currentGPS.hdg),
'Lat: ' + (currentGPS.lat !== null ? currentGPS.lat.toFixed(6) : '--') + ' Lng: ' + (currentGPS.lng !== null ? currentGPS.lng.toFixed(6) : '--'),
'Alt: ' + (currentGPS.alt !== null ? Math.round(currentGPS.alt)+'m' : '--') + ' Acc: ' + (currentGPS.acc !== null ? Math.round(currentGPS.acc)+'m' : '--')
]
});
}
function buildSRT() {
return srtEntries.map((e,i) => (i+1)+'\n'+toSRTTime(e.startMs)+' --> '+toSRTTime(e.endMs)+'\n'+e.lines.join('\n')).join('\n\n')+'\n';
}
function getFilename() {
const n = new Date();
return 'dashcam_'+n.getFullYear()+pad(n.getMonth()+1)+pad(n.getDate())+'_'+pad(n.getHours())+pad(n.getMinutes())+pad(n.getSeconds());
} }
function getBestMimeType() { function getBestMimeType() {
const types = [ const types = ['video/webm;codecs=vp9','video/webm;codecs=vp8','video/webm','video/mp4'];
'video/webm;codecs=vp9',
'video/webm;codecs=vp8',
'video/webm',
'video/mp4'
];
for (const t of types) { if (MediaRecorder.isTypeSupported(t)) return t; } for (const t of types) { if (MediaRecorder.isTypeSupported(t)) return t; }
return ''; return '';
} }
function saveRecording() { function dl(filename, blob) {
if (recordedChunks.length === 0) { setStatus('Nothing to save'); return; }
const mime = getBestMimeType();
const ext = mime.includes('mp4') ? 'mp4' : 'webm';
const blob = new Blob(recordedChunks, { type: mime });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
const now = new Date(); a.href = url; a.download = filename; a.click();
a.href = url;
a.download = 'dashcam_' + now.getFullYear() + pad(now.getMonth()+1) + pad(now.getDate()) + '_' + pad(now.getHours()) + pad(now.getMinutes()) + pad(now.getSeconds()) + '.' + ext;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 5000); setTimeout(() => URL.revokeObjectURL(url), 5000);
recordedChunks = [];
setStatus('Saved!');
} }
function toggleRecord() { function toggleRecord() {
if (!stream) return; if (!stream) return;
const btn = document.getElementById('rec-btn'); const btn = document.getElementById('rec-btn');
const ind = document.getElementById('rec-indicator'); const ind = document.getElementById('rec-indicator');
if (!isRecording) { if (!isRecording) {
recordedChunks = []; recordedChunks = []; srtEntries = []; recStartTime = Date.now();
const mime = getBestMimeType(); const mime = getBestMimeType();
try { try { mediaRecorder = new MediaRecorder(stream, mime ? { mimeType: mime } : {}); }
mediaRecorder = new MediaRecorder(stream, mime ? { mimeType: mime } : {}); catch(e) { mediaRecorder = new MediaRecorder(stream); }
} catch(e) {
mediaRecorder = new MediaRecorder(stream);
}
mediaRecorder.ondataavailable = e => { if (e.data && e.data.size > 0) recordedChunks.push(e.data); }; mediaRecorder.ondataavailable = e => { if (e.data && e.data.size > 0) recordedChunks.push(e.data); };
mediaRecorder.onstop = saveRecording; mediaRecorder.onstop = () => {
const name = getFilename();
const mime2 = getBestMimeType();
const ext = mime2.includes('mp4') ? 'mp4' : 'webm';
dl(name+'.'+ext, new Blob(recordedChunks, { type: mime2 }));
setTimeout(() => dl(name+'.srt', new Blob([buildSRT()], { type: 'text/plain' })), 800);
recordedChunks = [];
setStatus('Saved video + SRT');
};
mediaRecorder.start(1000); mediaRecorder.start(1000);
isRecording = true; isRecording = true; recSeconds = 0;
recSeconds = 0;
ind.style.display = 'flex'; ind.style.display = 'flex';
btn.textContent = '&#9646;&#9646; SAVE';
btn.innerHTML = '&#9646;&#9646; SAVE'; btn.innerHTML = '&#9646;&#9646; SAVE';
btn.classList.add('danger'); btn.classList.add('danger');
captureSRTFrame();
recInterval = setInterval(() => { recInterval = setInterval(() => {
recSeconds++; recSeconds++;
const m = pad(Math.floor(recSeconds/60)), s = pad(recSeconds%60); document.getElementById('rec-timer').textContent = pad(Math.floor(recSeconds/60))+':'+pad(recSeconds%60);
document.getElementById('rec-timer').textContent = m+':'+s; captureSRTFrame();
}, 1000); }, 1000);
setStatus('Recording...'); setStatus('Recording...');
} else { } else {
@@ -221,10 +295,38 @@ function toggleRecord() {
} }
} }
function startGPS() {
if (!navigator.geolocation) { setStatus('GPS unavailable'); return; }
document.getElementById('gps-bar').style.display = 'block';
watchId = navigator.geolocation.watchPosition(pos => {
const c = pos.coords;
currentGPS = { lat: c.latitude, lng: c.longitude, alt: c.altitude, acc: c.accuracy, hdg: c.heading, speed: c.speed !== null ? c.speed : 0 };
const conv = cvt(currentGPS.speed);
if (conv > maxSpeed) maxSpeed = conv;
const cur = { lat: c.latitude, lng: c.longitude };
if (lastPos) { const d = haversine(lastPos, cur); if (d < 200) totalDist += d; }
lastPos = cur;
document.getElementById('speed-val').textContent = Math.round(conv);
document.getElementById('lat-val').textContent = c.latitude.toFixed(5);
document.getElementById('lng-val').textContent = c.longitude.toFixed(5);
document.getElementById('alt-val').textContent = c.altitude !== null ? Math.round(c.altitude)+'m' : '--';
document.getElementById('acc-val').textContent = Math.round(c.accuracy)+'m';
document.getElementById('hdg-val').textContent = bearingLabel(c.heading);
document.getElementById('dist-val').textContent = (totalDist/1000).toFixed(2)+'km';
document.getElementById('gps-dot').className = 'active';
syncBigSpeedo();
setStatus('');
}, err => {
setStatus('GPS: ' + err.message);
document.getElementById('gps-dot').className = '';
}, { enableHighAccuracy: true, maximumAge: 1000, timeout: 15000 });
}
async function startDashcam() { async function startDashcam() {
const btn = document.getElementById('start-btn'); const btn = document.getElementById('start-btn');
btn.disabled = true; btn.textContent = '...'; btn.disabled = true; btn.textContent = '...';
if (isRecording) toggleRecord(); if (isRecording) toggleRecord();
closeSpeedo();
try { try {
if (stream) stream.getTracks().forEach(t => t.stop()); if (stream) stream.getTracks().forEach(t => t.stop());
stream = await navigator.mediaDevices.getUserMedia({ stream = await navigator.mediaDevices.getUserMedia({
@@ -238,6 +340,7 @@ async function startDashcam() {
btn.textContent = 'RESTART'; btn.disabled = false; btn.textContent = 'RESTART'; btn.disabled = false;
if (watchId !== null) { navigator.geolocation.clearWatch(watchId); watchId = null; } if (watchId !== null) { navigator.geolocation.clearWatch(watchId); watchId = null; }
maxSpeed = 0; totalDist = 0; lastPos = null; maxSpeed = 0; totalDist = 0; lastPos = null;
currentGPS = { lat: null, lng: null, alt: null, acc: null, hdg: null, speed: 0 };
startGPS(); startGPS();
if (clockInterval) clearInterval(clockInterval); if (clockInterval) clearInterval(clockInterval);
clockInterval = setInterval(updateClock, 1000); clockInterval = setInterval(updateClock, 1000);
@@ -249,15 +352,14 @@ async function startDashcam() {
} }
} }
function switchCamera() { function switchCamera() { facingMode = facingMode === 'environment' ? 'user' : 'environment'; startDashcam(); }
facingMode = facingMode === 'environment' ? 'user' : 'environment';
startDashcam();
}
function toggleUnit() { function toggleUnit() {
useMph = !useMph; useMph = !useMph;
maxSpeed = useMph ? maxSpeed / 3.6 * 2.23694 : maxSpeed / 2.23694 * 3.6; maxSpeed = useMph ? maxSpeed / 3.6 * 2.23694 : maxSpeed / 2.23694 * 3.6;
document.getElementById('speed-unit').textContent = useMph ? 'MPH' : 'KPH'; const unit = useMph ? 'MPH' : 'KPH';
document.getElementById('speed-unit').textContent = unit;
document.getElementById('big-speed-unit').textContent = unit;
} }
function toggleFullscreen() { function toggleFullscreen() {
@@ -273,10 +375,7 @@ function toggleFullscreen() {
btn.textContent = '\u26f6 FULL'; btn.textContent = '\u26f6 FULL';
} }
document.addEventListener('fullscreenchange', () => { document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) { if (!document.fullscreenElement) { app.classList.remove('fullscreen'); btn.textContent = '\u26f6 FULL'; }
app.classList.remove('fullscreen');
btn.textContent = '\u26f6 FULL';
}
}, { once: true }); }, { once: true });
} }
</script> </script>