Added speedo pop up / full screen
This commit is contained in:
206
dashcam.html
206
dashcam.html
@@ -14,12 +14,45 @@
|
|||||||
#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} }
|
||||||
|
|
||||||
#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; }
|
||||||
@@ -31,7 +64,7 @@
|
|||||||
#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; }
|
||||||
@@ -43,26 +76,50 @@
|
|||||||
|
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<video id="video" autoplay playsinline muted></video>
|
<video id="video" autoplay playsinline muted></video>
|
||||||
|
|
||||||
<div id="placeholder">
|
<div id="placeholder">
|
||||||
<div style="font-size:40px">🎥</div>
|
<div style="font-size:40px">🎥</div>
|
||||||
<div style="color:#aaa;font-family:var(--font-sans)">Tap START to begin</div>
|
<div style="color:#aaa;font-family:var(--font-sans)">Tap START to begin</div>
|
||||||
<div style="font-size:11px;color:#555;font-family:var(--font-sans)">Camera + location permissions required</div>
|
<div style="font-size:11px;color:#555;font-family:var(--font-sans)">Camera + location permissions required</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="hud">
|
<div id="hud">
|
||||||
<div id="top-left">
|
<div id="top-left">
|
||||||
<div id="clock">--:--:--</div>
|
<div id="clock">--:--:--</div>
|
||||||
<div id="dateline">---</div>
|
<div id="dateline">---</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="rec-indicator">
|
<div id="rec-indicator">
|
||||||
<div id="rec-dot"></div>
|
<div id="rec-dot"></div>
|
||||||
<span>REC</span>
|
<span>REC</span>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
|
<!-- big speedo overlay -->
|
||||||
|
<div id="speedo-overlay" onclick="closeSpeedo()">
|
||||||
|
<button id="speedo-close" onclick="closeSpeedo()">✕</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 id="gps-bar">
|
<div id="gps-bar">
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<div id="gps-dot"></div>
|
<div id="gps-dot"></div>
|
||||||
@@ -74,7 +131,9 @@
|
|||||||
<div class="di"><span class="dl">DIST</span><span class="dv" id="dist-val">0.00km</span></div>
|
<div class="di"><span class="dl">DIST</span><span class="dv" id="dist-val">0.00km</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="status-bar"></div>
|
<div id="status-bar"></div>
|
||||||
|
|
||||||
<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>
|
||||||
@@ -92,11 +151,8 @@ 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;
|
||||||
// SRT state
|
let srtEntries = [], recStartTime = null;
|
||||||
let srtEntries = [];
|
|
||||||
let srtInterval = null;
|
|
||||||
let 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 };
|
||||||
|
|
||||||
function cvt(ms) { return useMph ? ms * 2.23694 : ms * 3.6; }
|
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';
|
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 pad3(v) { return String(Math.floor(v)).padStart(3,'0'); }
|
||||||
|
|
||||||
function updateClock() {
|
function updateClock() {
|
||||||
@@ -127,57 +183,61 @@ function updateClock() {
|
|||||||
|
|
||||||
function setStatus(msg) { document.getElementById('status-bar').textContent = msg; }
|
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) {
|
function toSRTTime(ms) {
|
||||||
const h = Math.floor(ms / 3600000);
|
const h = Math.floor(ms/3600000), m = Math.floor((ms%3600000)/60000);
|
||||||
const m = Math.floor((ms % 3600000) / 60000);
|
const s = Math.floor((ms%60000)/1000), mil = ms%1000;
|
||||||
const s = Math.floor((ms % 60000) / 1000);
|
|
||||||
const mil = ms % 1000;
|
|
||||||
return pad(h)+':'+pad(m)+':'+pad(s)+','+pad3(mil);
|
return pad(h)+':'+pad(m)+':'+pad(s)+','+pad3(mil);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture one SRT frame every second during recording
|
|
||||||
function captureSRTFrame() {
|
function captureSRTFrame() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const elapsed = Date.now() - recStartTime;
|
const elapsed = Date.now() - recStartTime;
|
||||||
const dateStr = now.getFullYear()+'-'+pad(now.getMonth()+1)+'-'+pad(now.getDate());
|
const dateStr = now.getFullYear()+'-'+pad(now.getMonth()+1)+'-'+pad(now.getDate());
|
||||||
const timeStr = pad(now.getHours())+':'+pad(now.getMinutes())+':'+pad(now.getSeconds());
|
const timeStr = pad(now.getHours())+':'+pad(now.getMinutes())+':'+pad(now.getSeconds());
|
||||||
const spd = currentGPS.speed !== null ? Math.round(cvt(currentGPS.speed)) : '--';
|
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({
|
srtEntries.push({
|
||||||
startMs: elapsed,
|
startMs: elapsed, endMs: elapsed + 1000,
|
||||||
endMs: elapsed + 1000,
|
|
||||||
lines: [
|
lines: [
|
||||||
dateStr + ' ' + timeStr,
|
dateStr + ' ' + timeStr,
|
||||||
'Speed: ' + spd + ' ' + cvtLabel() + ' Hdg: ' + hdg,
|
'Speed: ' + spd + ' ' + cvtLabel() + ' Hdg: ' + bearingLabel(currentGPS.hdg),
|
||||||
'Lat: ' + lat + ' Lng: ' + lng,
|
'Lat: ' + (currentGPS.lat !== null ? currentGPS.lat.toFixed(6) : '--') + ' Lng: ' + (currentGPS.lng !== null ? currentGPS.lng.toFixed(6) : '--'),
|
||||||
'Alt: ' + alt + ' Acc: ' + acc
|
'Alt: ' + (currentGPS.alt !== null ? Math.round(currentGPS.alt)+'m' : '--') + ' Acc: ' + (currentGPS.acc !== null ? Math.round(currentGPS.acc)+'m' : '--')
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSRT() {
|
function buildSRT() {
|
||||||
return srtEntries.map((e, i) => {
|
return srtEntries.map((e,i) => (i+1)+'\n'+toSRTTime(e.startMs)+' --> '+toSRTTime(e.endMs)+'\n'+e.lines.join('\n')).join('\n\n')+'\n';
|
||||||
return (i+1) + '\n' +
|
|
||||||
toSRTTime(e.startMs) + ' --> ' + toSRTTime(e.endMs) + '\n' +
|
|
||||||
e.lines.join('\n');
|
|
||||||
}).join('\n\n') + '\n';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSRT(basename) {
|
function getFilename() {
|
||||||
if (srtEntries.length === 0) return;
|
const n = new Date();
|
||||||
const blob = new Blob([buildSRT()], { type: 'text/plain' });
|
return 'dashcam_'+n.getFullYear()+pad(n.getMonth()+1)+pad(n.getDate())+'_'+pad(n.getHours())+pad(n.getMinutes())+pad(n.getSeconds());
|
||||||
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 getBestMimeType() {
|
function getBestMimeType() {
|
||||||
@@ -186,58 +246,41 @@ function getBestMimeType() {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilename() {
|
function dl(filename, blob) {
|
||||||
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 });
|
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url; a.download = filename; a.click();
|
||||||
a.download = basename + '.' + ext;
|
|
||||||
a.click();
|
|
||||||
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
||||||
recordedChunks = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
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 = () => {
|
mediaRecorder.onstop = () => {
|
||||||
const name = getFilename();
|
const name = getFilename();
|
||||||
saveVideo(name);
|
const mime2 = getBestMimeType();
|
||||||
setTimeout(() => saveSRT(name), 800);
|
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');
|
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.innerHTML = '▮▮ SAVE';
|
btn.innerHTML = '▮▮ SAVE';
|
||||||
btn.classList.add('danger');
|
btn.classList.add('danger');
|
||||||
captureSRTFrame();
|
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();
|
captureSRTFrame();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
setStatus('Recording...');
|
setStatus('Recording...');
|
||||||
@@ -254,7 +297,7 @@ 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 = 'flex';
|
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 };
|
||||||
@@ -264,7 +307,6 @@ function startGPS() {
|
|||||||
if (lastPos) { const d = haversine(lastPos, cur); if (d < 200) totalDist += d; }
|
if (lastPos) { const d = haversine(lastPos, cur); if (d < 200) totalDist += d; }
|
||||||
lastPos = cur;
|
lastPos = cur;
|
||||||
document.getElementById('speed-val').textContent = Math.round(conv);
|
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('lat-val').textContent = c.latitude.toFixed(5);
|
||||||
document.getElementById('lng-val').textContent = c.longitude.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('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('hdg-val').textContent = bearingLabel(c.heading);
|
||||||
document.getElementById('dist-val').textContent = (totalDist/1000).toFixed(2)+'km';
|
document.getElementById('dist-val').textContent = (totalDist/1000).toFixed(2)+'km';
|
||||||
document.getElementById('gps-dot').className = 'active';
|
document.getElementById('gps-dot').className = 'active';
|
||||||
|
syncBigSpeedo();
|
||||||
setStatus('');
|
setStatus('');
|
||||||
}, err => {
|
}, err => {
|
||||||
setStatus('GPS: ' + err.message);
|
setStatus('GPS: ' + err.message);
|
||||||
@@ -283,6 +326,7 @@ 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({
|
||||||
@@ -308,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() {
|
||||||
@@ -332,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>
|
||||||
|
|||||||
Reference in New Issue
Block a user