Added picture option (whilst recording as well)

This commit is contained in:
Jon
2026-04-29 22:16:13 +01:00
parent a58177c8cd
commit e85b0fc51c

View File

@@ -14,35 +14,19 @@
#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; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} } @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} }
/* 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; }
#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-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; }
/* BIG speedo overlay */ /* flash effect on snapshot */
#speedo-overlay { #flash { position: absolute; inset: 0; background: #fff; opacity: 0; pointer-events: none; z-index: 30; transition: opacity 0.05s; }
position: absolute; inset: 0; #flash.on { opacity: 0.85; }
background: rgba(0,0,0,0.92);
display: none; flex-direction: column; /* big speedo */
align-items: center; justify-content: center; #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; }
pointer-events: all; cursor: pointer;
z-index: 10;
}
#speedo-overlay.visible { display: flex; } #speedo-overlay.visible { display: flex; }
#speedo-close { #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; }
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; } #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-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-speed-unit { font-size: clamp(14px, 3vw, 22px); color: #aaa; letter-spacing: 3px; margin-top: 8px; text-align: center; }
@@ -52,7 +36,8 @@
.bdl { color: #555; font-size: clamp(9px, 1.5vw, 12px); letter-spacing: 0.5px; } .bdl { color: #555; font-size: clamp(9px, 1.5vw, 12px); letter-spacing: 0.5px; }
.bdv { color: #fff; font-weight: 500; } .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; } /* info bar */
#gps-bar { position: absolute; bottom: 48px; left: 0; right: 0; background: rgba(0,0,0,0.55); 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; }
@@ -64,18 +49,23 @@
#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; z-index: 20; } /* button bar */
.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; } #btn-bar { position: absolute; bottom: 0; left: 0; right: 0; display: flex; justify-content: center; align-items: center; gap: 8px; padding: 7px 10px; 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 12px; 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; }
.hud-btn.primary { background: rgba(29,158,117,0.7); border-color: #1D9E75; } .hud-btn.primary { background: rgba(29,158,117,0.7); border-color: #1D9E75; }
.hud-btn.primary:hover { background: rgba(29,158,117,0.9); } .hud-btn.primary:hover { background: rgba(29,158,117,0.9); }
.hud-btn.danger { background: rgba(180,0,0,0.7); border-color: rgba(220,0,0,0.9); } .hud-btn.danger { background: rgba(180,0,0,0.7); border-color: rgba(220,0,0,0.9); }
.hud-btn.danger:hover { background: rgba(220,0,0,0.85); } .hud-btn.danger:hover { background: rgba(220,0,0,0.85); }
.hud-btn.active { background: rgba(186,117,23,0.7); border-color: rgba(186,117,23,0.9); }
canvas { display: none; }
</style> </style>
<div id="app"> <div id="app">
<video id="video" autoplay playsinline muted></video> <video id="video" autoplay playsinline muted></video>
<canvas id="snap-canvas"></canvas>
<div id="flash"></div>
<div id="placeholder"> <div id="placeholder">
<div style="font-size:40px">&#127909;</div> <div style="font-size:40px">&#127909;</div>
@@ -95,13 +85,11 @@
<span id="rec-timer">00:00</span> <span id="rec-timer">00:00</span>
</div> </div>
<!-- small speedo, tappable when not recording -->
<div id="speed-display" onclick="openSpeedo()"> <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> </div>
<!-- big speedo overlay -->
<div id="speedo-overlay" onclick="closeSpeedo()"> <div id="speedo-overlay" onclick="closeSpeedo()">
<button id="speedo-close" onclick="closeSpeedo()">&#x2715;</button> <button id="speedo-close" onclick="closeSpeedo()">&#x2715;</button>
<div id="big-speed-val">--</div> <div id="big-speed-val">--</div>
@@ -137,9 +125,11 @@
<div id="btn-bar"> <div id="btn-bar">
<button class="hud-btn primary" id="start-btn" onclick="startDashcam()">START</button> <button class="hud-btn primary" id="start-btn" onclick="startDashcam()">START</button>
<button class="hud-btn" id="flip-btn" onclick="switchCamera()" disabled>FLIP</button> <button class="hud-btn" id="flip-btn" onclick="switchCamera()" disabled>FLIP</button>
<button class="hud-btn" id="rec-btn" onclick="toggleRecord()" disabled>&#9679; RECORD</button> <button class="hud-btn" id="snap-btn" onclick="takeSnapshot()" disabled>&#128247;</button>
<button class="hud-btn" id="rec-btn" onclick="toggleRecord()" disabled>&#9679; REC</button>
<button class="hud-btn" id="info-btn" onclick="toggleInfo()" disabled>INFO</button>
<button class="hud-btn" id="unit-btn" onclick="toggleUnit()">MPH/KPH</button> <button class="hud-btn" id="unit-btn" onclick="toggleUnit()">MPH/KPH</button>
<button class="hud-btn" id="fs-btn" onclick="toggleFullscreen()">&#x26F6; FULL</button> <button class="hud-btn" id="fs-btn" onclick="toggleFullscreen()">&#x26F6;</button>
</div> </div>
</div> </div>
</div> </div>
@@ -151,7 +141,7 @@ let useMph = true, maxSpeed = 0;
let lastPos = null, totalDist = 0; let lastPos = null, totalDist = 0;
let mediaRecorder = null, recordedChunks = []; let mediaRecorder = null, recordedChunks = [];
let recInterval = null, recSeconds = 0, isRecording = false; let recInterval = null, recSeconds = 0, isRecording = false;
let speedoOpen = false; let speedoOpen = false, infoVisible = false;
let srtEntries = [], recStartTime = null; let srtEntries = [], recStartTime = null;
let currentGPS = { lat: null, lng: null, alt: null, acc: null, hdg: null, speed: 0 }; let currentGPS = { lat: null, lng: null, alt: null, acc: null, hdg: null, speed: 0 };
@@ -183,6 +173,60 @@ function updateClock() {
function setStatus(msg) { document.getElementById('status-bar').textContent = msg; } function setStatus(msg) { document.getElementById('status-bar').textContent = msg; }
function toggleInfo() {
infoVisible = !infoVisible;
const bar = document.getElementById('gps-bar');
const btn = document.getElementById('info-btn');
bar.style.display = infoVisible ? 'block' : 'none';
btn.classList.toggle('active', infoVisible);
btn.textContent = infoVisible ? 'INFO ✓' : 'INFO';
}
function takeSnapshot() {
if (!stream) return;
const video = document.getElementById('video');
const canvas = document.getElementById('snap-canvas');
canvas.width = video.videoWidth || 1280;
canvas.height = video.videoHeight || 720;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// stamp telemetry onto image
const n = new Date();
const dateStr = n.getFullYear()+'-'+pad(n.getMonth()+1)+'-'+pad(n.getDate())+' '+pad(n.getHours())+':'+pad(n.getMinutes())+':'+pad(n.getSeconds());
const spd = currentGPS.speed !== null ? Math.round(cvt(currentGPS.speed))+' '+cvtLabel().toUpperCase() : '--';
const lat = currentGPS.lat !== null ? currentGPS.lat.toFixed(6) : '--';
const lng = currentGPS.lng !== null ? currentGPS.lng.toFixed(6) : '--';
const alt = currentGPS.alt !== null ? Math.round(currentGPS.alt)+'m' : '--';
const lines = [dateStr, spd+' '+bearingLabel(currentGPS.hdg), 'LAT '+lat+' LNG '+lng, 'ALT '+alt];
ctx.font = 'bold 20px monospace';
ctx.textBaseline = 'bottom';
const lineH = 26, pad8 = 10;
const boxH = lines.length * lineH + pad8 * 2;
ctx.fillStyle = 'rgba(0,0,0,0.55)';
ctx.fillRect(0, canvas.height - boxH, canvas.width, boxH);
ctx.fillStyle = '#ffffff';
lines.forEach((l, i) => {
ctx.fillText(l, pad8, canvas.height - boxH + pad8 + (i+1)*lineH);
});
// flash
const flash = document.getElementById('flash');
flash.classList.add('on');
setTimeout(() => flash.classList.remove('on'), 120);
// download
canvas.toBlob(blob => {
const fname = 'dashcam_snap_'+n.getFullYear()+pad(n.getMonth()+1)+pad(n.getDate())+'_'+pad(n.getHours())+pad(n.getMinutes())+pad(n.getSeconds())+'.jpg';
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = fname; a.click();
setTimeout(() => URL.revokeObjectURL(url), 5000);
setStatus('Snapshot saved');
}, 'image/jpeg', 0.92);
}
function openSpeedo() { function openSpeedo() {
if (isRecording) return; if (isRecording) return;
speedoOpen = true; speedoOpen = true;
@@ -209,8 +253,7 @@ function syncBigSpeedo() {
} }
function toSRTTime(ms) { function toSRTTime(ms) {
const h = Math.floor(ms/3600000), m = Math.floor((ms%3600000)/60000); const h=Math.floor(ms/3600000), m=Math.floor((ms%3600000)/60000), s=Math.floor((ms%60000)/1000), mil=ms%1000;
const s = Math.floor((ms%60000)/1000), mil = ms%1000;
return pad(h)+':'+pad(m)+':'+pad(s)+','+pad3(mil); return pad(h)+':'+pad(m)+':'+pad(s)+','+pad3(mil);
} }
@@ -247,8 +290,7 @@ function getBestMimeType() {
} }
function dl(filename, blob) { function dl(filename, blob) {
const url = URL.createObjectURL(blob); const url=URL.createObjectURL(blob), a=document.createElement('a');
const a = document.createElement('a');
a.href=url; a.download=filename; a.click(); a.href=url; a.download=filename; a.click();
setTimeout(()=>URL.revokeObjectURL(url),5000); setTimeout(()=>URL.revokeObjectURL(url),5000);
} }
@@ -264,9 +306,7 @@ function toggleRecord() {
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=()=>{ mediaRecorder.onstop=()=>{
const name = getFilename(); const name=getFilename(), mime2=getBestMimeType(), ext=mime2.includes('mp4')?'mp4':'webm';
const mime2 = getBestMimeType();
const ext = mime2.includes('mp4') ? 'mp4' : 'webm';
dl(name+'.'+ext, new Blob(recordedChunks,{type:mime2})); dl(name+'.'+ext, new Blob(recordedChunks,{type:mime2}));
setTimeout(()=>dl(name+'.srt',new Blob([buildSRT()],{type:'text/plain'})),800); setTimeout(()=>dl(name+'.srt',new Blob([buildSRT()],{type:'text/plain'})),800);
recordedChunks=[]; recordedChunks=[];
@@ -285,11 +325,10 @@ function toggleRecord() {
},1000); },1000);
setStatus('Recording...'); setStatus('Recording...');
} else { } else {
mediaRecorder.stop(); mediaRecorder.stop(); isRecording=false;
isRecording = false;
clearInterval(recInterval); clearInterval(recInterval);
ind.style.display='none'; ind.style.display='none';
btn.innerHTML = '&#9679; RECORD'; btn.innerHTML='&#9679; REC';
btn.classList.remove('danger'); btn.classList.remove('danger');
setStatus('Saving...'); setStatus('Saving...');
} }
@@ -297,7 +336,6 @@ function toggleRecord() {
function startGPS() { function startGPS() {
if (!navigator.geolocation) { setStatus('GPS unavailable'); return; } if (!navigator.geolocation) { setStatus('GPS unavailable'); return; }
document.getElementById('gps-bar').style.display = 'block';
watchId=navigator.geolocation.watchPosition(pos=>{ watchId=navigator.geolocation.watchPosition(pos=>{
const c=pos.coords; 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}; currentGPS={lat:c.latitude,lng:c.longitude,alt:c.altitude,acc:c.accuracy,hdg:c.heading,speed:c.speed!==null?c.speed:0};
@@ -329,14 +367,14 @@ async function startDashcam() {
closeSpeedo(); 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({video:{facingMode,width:{ideal:1920},height:{ideal:1080}},audio:false});
video: { facingMode, width: { ideal: 1920 }, height: { ideal: 1080 } }, audio: false
});
document.getElementById('video').srcObject=stream; document.getElementById('video').srcObject=stream;
document.getElementById('placeholder').style.display='none'; document.getElementById('placeholder').style.display='none';
document.getElementById('top-left').style.display='flex'; document.getElementById('top-left').style.display='flex';
document.getElementById('flip-btn').disabled=false; document.getElementById('flip-btn').disabled=false;
document.getElementById('rec-btn').disabled=false; document.getElementById('rec-btn').disabled=false;
document.getElementById('snap-btn').disabled=false;
document.getElementById('info-btn').disabled=false;
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;
@@ -363,19 +401,16 @@ function toggleUnit() {
} }
function toggleFullscreen() { function toggleFullscreen() {
const app = document.getElementById('app'); const app=document.getElementById('app'), btn=document.getElementById('fs-btn');
const btn = document.getElementById('fs-btn');
if(!app.classList.contains('fullscreen')){ if(!app.classList.contains('fullscreen')){
if(app.requestFullscreen) app.requestFullscreen().catch(()=>{}); if(app.requestFullscreen) app.requestFullscreen().catch(()=>{});
app.classList.add('fullscreen'); app.classList.add('fullscreen'); btn.textContent='\u2715';
btn.textContent = '\u2715 EXIT';
} else { } else {
if(document.exitFullscreen&&document.fullscreenElement) document.exitFullscreen().catch(()=>{}); if(document.exitFullscreen&&document.fullscreenElement) document.exitFullscreen().catch(()=>{});
app.classList.remove('fullscreen'); app.classList.remove('fullscreen'); btn.textContent='\u26f6';
btn.textContent = '\u26f6 FULL';
} }
document.addEventListener('fullscreenchange',()=>{ document.addEventListener('fullscreenchange',()=>{
if (!document.fullscreenElement) { app.classList.remove('fullscreen'); btn.textContent = '\u26f6 FULL'; } if(!document.fullscreenElement){app.classList.remove('fullscreen');btn.textContent='\u26f6';}
},{once:true}); },{once:true});
} }
</script> </script>