+
+
+
+
+
+
+
+
--
+
MPH
+
+
+
+
ALT--
+
ACC--
+
HDG--
+
DIST--
+
+
+
+
@@ -74,7 +131,9 @@
DIST0.00km
+
+
@@ -92,11 +151,8 @@ let useMph = true, maxSpeed = 0;
let lastPos = null, totalDist = 0;
let mediaRecorder = null, recordedChunks = [];
let recInterval = null, recSeconds = 0, isRecording = false;
-
-// SRT state
-let srtEntries = [];
-let srtInterval = null;
-let recStartTime = null;
+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; }
@@ -114,7 +170,7 @@ function bearingLabel(deg) {
return ['N','NE','E','SE','S','SW','W','NW'][Math.round(deg/45)%8] + ' ' + Math.round(deg) + '\xb0';
}
-function pad(v, n=2) { return String(Math.floor(v)).padStart(n,'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() {
@@ -127,57 +183,61 @@ function updateClock() {
function setStatus(msg) { document.getElementById('status-bar').textContent = msg; }
-// Format ms offset as SRT timecode HH:MM:SS,mmm
+function openSpeedo() {
+ if (isRecording) return;
+ speedoOpen = true;
+ document.getElementById('speedo-overlay').classList.add('visible');
+ syncBigSpeedo();
+}
+
+function closeSpeedo() {
+ speedoOpen = false;
+ document.getElementById('speedo-overlay').classList.remove('visible');
+}
+
+function syncBigSpeedo() {
+ if (!speedoOpen) return;
+ const unit = useMph ? 'MPH' : 'KPH';
+ document.getElementById('big-speed-val').textContent = document.getElementById('speed-val').textContent;
+ document.getElementById('big-speed-unit').textContent = unit;
+ document.getElementById('b-lat').textContent = document.getElementById('lat-val').textContent;
+ document.getElementById('b-lng').textContent = document.getElementById('lng-val').textContent;
+ document.getElementById('b-alt').textContent = document.getElementById('alt-val').textContent;
+ document.getElementById('b-acc').textContent = document.getElementById('acc-val').textContent;
+ document.getElementById('b-hdg').textContent = document.getElementById('hdg-val').textContent;
+ document.getElementById('b-dist').textContent = document.getElementById('dist-val').textContent;
+}
+
function toSRTTime(ms) {
- const h = Math.floor(ms / 3600000);
- const m = Math.floor((ms % 3600000) / 60000);
- const s = Math.floor((ms % 60000) / 1000);
- const mil = ms % 1000;
+ 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);
}
-// Capture one SRT frame every second during recording
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)) : '--';
- 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 acc = currentGPS.acc !== null ? Math.round(currentGPS.acc)+'m' : '--';
- const hdg = currentGPS.hdg !== null ? bearingLabel(currentGPS.hdg) : '--';
-
srtEntries.push({
- startMs: elapsed,
- endMs: elapsed + 1000,
+ startMs: elapsed, endMs: elapsed + 1000,
lines: [
dateStr + ' ' + timeStr,
- 'Speed: ' + spd + ' ' + cvtLabel() + ' Hdg: ' + hdg,
- 'Lat: ' + lat + ' Lng: ' + lng,
- 'Alt: ' + alt + ' Acc: ' + acc
+ '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) => {
- return (i+1) + '\n' +
- toSRTTime(e.startMs) + ' --> ' + toSRTTime(e.endMs) + '\n' +
- e.lines.join('\n');
- }).join('\n\n') + '\n';
+ return srtEntries.map((e,i) => (i+1)+'\n'+toSRTTime(e.startMs)+' --> '+toSRTTime(e.endMs)+'\n'+e.lines.join('\n')).join('\n\n')+'\n';
}
-function saveSRT(basename) {
- if (srtEntries.length === 0) return;
- const blob = new Blob([buildSRT()], { type: 'text/plain' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = basename + '.srt';
- a.click();
- setTimeout(() => URL.revokeObjectURL(url), 5000);
+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() {
@@ -186,58 +246,41 @@ function getBestMimeType() {
return '';
}
-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 saveVideo(basename) {
- 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 });
+function dl(filename, blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
- a.href = url;
- a.download = basename + '.' + ext;
- a.click();
+ a.href = url; a.download = filename; a.click();
setTimeout(() => URL.revokeObjectURL(url), 5000);
- recordedChunks = [];
}
function toggleRecord() {
if (!stream) return;
const btn = document.getElementById('rec-btn');
const ind = document.getElementById('rec-indicator');
-
if (!isRecording) {
- recordedChunks = [];
- srtEntries = [];
- recStartTime = Date.now();
+ recordedChunks = []; srtEntries = []; recStartTime = Date.now();
const mime = getBestMimeType();
- try {
- mediaRecorder = new MediaRecorder(stream, mime ? { mimeType: mime } : {});
- } catch(e) {
- mediaRecorder = new MediaRecorder(stream);
- }
+ try { mediaRecorder = new MediaRecorder(stream, mime ? { mimeType: mime } : {}); }
+ catch(e) { mediaRecorder = new MediaRecorder(stream); }
mediaRecorder.ondataavailable = e => { if (e.data && e.data.size > 0) recordedChunks.push(e.data); };
mediaRecorder.onstop = () => {
const name = getFilename();
- saveVideo(name);
- setTimeout(() => saveSRT(name), 800);
+ 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);
- isRecording = true;
- recSeconds = 0;
+ isRecording = true; recSeconds = 0;
ind.style.display = 'flex';
btn.innerHTML = '▮▮ SAVE';
btn.classList.add('danger');
captureSRTFrame();
recInterval = setInterval(() => {
recSeconds++;
- const m = pad(Math.floor(recSeconds/60)), s = pad(recSeconds%60);
- document.getElementById('rec-timer').textContent = m+':'+s;
+ document.getElementById('rec-timer').textContent = pad(Math.floor(recSeconds/60))+':'+pad(recSeconds%60);
captureSRTFrame();
}, 1000);
setStatus('Recording...');
@@ -254,7 +297,7 @@ function toggleRecord() {
function startGPS() {
if (!navigator.geolocation) { setStatus('GPS unavailable'); return; }
- document.getElementById('gps-bar').style.display = 'flex';
+ 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 };
@@ -264,7 +307,6 @@ function startGPS() {
if (lastPos) { const d = haversine(lastPos, cur); if (d < 200) totalDist += d; }
lastPos = cur;
document.getElementById('speed-val').textContent = Math.round(conv);
- document.getElementById('max-speed').textContent = 'MAX ' + Math.round(maxSpeed);
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' : '--';
@@ -272,6 +314,7 @@ function startGPS() {
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);
@@ -283,6 +326,7 @@ async function startDashcam() {
const btn = document.getElementById('start-btn');
btn.disabled = true; btn.textContent = '...';
if (isRecording) toggleRecord();
+ closeSpeedo();
try {
if (stream) stream.getTracks().forEach(t => t.stop());
stream = await navigator.mediaDevices.getUserMedia({
@@ -308,15 +352,14 @@ async function startDashcam() {
}
}
-function switchCamera() {
- facingMode = facingMode === 'environment' ? 'user' : 'environment';
- startDashcam();
-}
+function switchCamera() { facingMode = facingMode === 'environment' ? 'user' : 'environment'; startDashcam(); }
function toggleUnit() {
useMph = !useMph;
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() {
@@ -332,10 +375,7 @@ function toggleFullscreen() {
btn.textContent = '\u26f6 FULL';
}
document.addEventListener('fullscreenchange', () => {
- if (!document.fullscreenElement) {
- app.classList.remove('fullscreen');
- btn.textContent = '\u26f6 FULL';
- }
+ if (!document.fullscreenElement) { app.classList.remove('fullscreen'); btn.textContent = '\u26f6 FULL'; }
}, { once: true });
}