Added subtitles of info
This commit is contained in:
219
dashcam.html
219
dashcam.html
@@ -12,7 +12,6 @@
|
||||
|
||||
#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-timer { font-size: 11px; }
|
||||
@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; }
|
||||
@@ -30,7 +29,6 @@
|
||||
@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; }
|
||||
|
||||
#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; }
|
||||
@@ -45,31 +43,26 @@
|
||||
|
||||
<div id="app">
|
||||
<video id="video" autoplay playsinline muted></video>
|
||||
|
||||
<div id="placeholder">
|
||||
<div style="font-size:40px">🎥</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>
|
||||
|
||||
<div id="hud">
|
||||
<div id="top-left">
|
||||
<div id="clock">--:--:--</div>
|
||||
<div id="dateline">---</div>
|
||||
</div>
|
||||
|
||||
<div id="rec-indicator">
|
||||
<div id="rec-dot"></div>
|
||||
<span>REC</span>
|
||||
<span id="rec-timer">00:00</span>
|
||||
</div>
|
||||
|
||||
<div id="speed-display">
|
||||
<div id="speed-val">--</div>
|
||||
<div id="speed-unit">MPH</div>
|
||||
<div id="max-speed">MAX --</div>
|
||||
</div>
|
||||
|
||||
<div id="gps-bar">
|
||||
<div class="data-row">
|
||||
<div id="gps-dot"></div>
|
||||
@@ -81,9 +74,7 @@
|
||||
<div class="di"><span class="dl">DIST</span><span class="dv" id="dist-val">0.00km</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status-bar"></div>
|
||||
|
||||
<div id="btn-bar">
|
||||
<button class="hud-btn primary" id="start-btn" onclick="startDashcam()">START</button>
|
||||
<button class="hud-btn" id="flip-btn" onclick="switchCamera()" disabled>FLIP</button>
|
||||
@@ -99,10 +90,17 @@ let stream = null, facingMode = 'environment';
|
||||
let watchId = null, clockInterval = null;
|
||||
let useMph = true, maxSpeed = 0;
|
||||
let lastPos = null, totalDist = 0;
|
||||
let mediaRecorder = null, recordedChunks = [], recInterval = null, recSeconds = 0;
|
||||
let isRecording = false;
|
||||
let mediaRecorder = null, recordedChunks = [];
|
||||
let recInterval = null, recSeconds = 0, isRecording = false;
|
||||
|
||||
// SRT state
|
||||
let srtEntries = [];
|
||||
let srtInterval = null;
|
||||
let 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 cvtLabel() { return useMph ? 'mph' : 'kph'; }
|
||||
|
||||
function haversine(a, b) {
|
||||
const R = 6371000, r = Math.PI / 180;
|
||||
@@ -116,7 +114,8 @@ function bearingLabel(deg) {
|
||||
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, n=2) { return String(Math.floor(v)).padStart(n,'0'); }
|
||||
function pad3(v) { return String(Math.floor(v)).padStart(3,'0'); }
|
||||
|
||||
function updateClock() {
|
||||
const n = new Date();
|
||||
@@ -128,13 +127,138 @@ function updateClock() {
|
||||
|
||||
function setStatus(msg) { document.getElementById('status-bar').textContent = msg; }
|
||||
|
||||
// Format ms offset as SRT timecode HH:MM:SS,mmm
|
||||
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;
|
||||
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,
|
||||
lines: [
|
||||
dateStr + ' ' + timeStr,
|
||||
'Speed: ' + spd + ' ' + cvtLabel() + ' Hdg: ' + hdg,
|
||||
'Lat: ' + lat + ' Lng: ' + lng,
|
||||
'Alt: ' + alt + ' Acc: ' + acc
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
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 getBestMimeType() {
|
||||
const types = ['video/webm;codecs=vp9','video/webm;codecs=vp8','video/webm','video/mp4'];
|
||||
for (const t of types) { if (MediaRecorder.isTypeSupported(t)) return t; }
|
||||
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 });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = basename + '.' + ext;
|
||||
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();
|
||||
const mime = getBestMimeType();
|
||||
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);
|
||||
setStatus('Saved video + SRT');
|
||||
};
|
||||
mediaRecorder.start(1000);
|
||||
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;
|
||||
captureSRTFrame();
|
||||
}, 1000);
|
||||
setStatus('Recording...');
|
||||
} else {
|
||||
mediaRecorder.stop();
|
||||
isRecording = false;
|
||||
clearInterval(recInterval);
|
||||
ind.style.display = 'none';
|
||||
btn.innerHTML = '● RECORD';
|
||||
btn.classList.remove('danger');
|
||||
setStatus('Saving...');
|
||||
}
|
||||
}
|
||||
|
||||
function startGPS() {
|
||||
if (!navigator.geolocation) { setStatus('GPS unavailable'); return; }
|
||||
document.getElementById('gps-bar').style.display = 'flex';
|
||||
watchId = navigator.geolocation.watchPosition(pos => {
|
||||
const c = pos.coords;
|
||||
const spd = c.speed !== null && c.speed >= 0 ? c.speed : 0;
|
||||
const conv = cvt(spd);
|
||||
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; }
|
||||
@@ -155,72 +279,6 @@ function startGPS() {
|
||||
}, { enableHighAccuracy: true, maximumAge: 1000, timeout: 15000 });
|
||||
}
|
||||
|
||||
function getBestMimeType() {
|
||||
const types = [
|
||||
'video/webm;codecs=vp9',
|
||||
'video/webm;codecs=vp8',
|
||||
'video/webm',
|
||||
'video/mp4'
|
||||
];
|
||||
for (const t of types) { if (MediaRecorder.isTypeSupported(t)) return t; }
|
||||
return '';
|
||||
}
|
||||
|
||||
function saveRecording() {
|
||||
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 a = document.createElement('a');
|
||||
const now = new Date();
|
||||
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);
|
||||
recordedChunks = [];
|
||||
setStatus('Saved!');
|
||||
}
|
||||
|
||||
function toggleRecord() {
|
||||
if (!stream) return;
|
||||
const btn = document.getElementById('rec-btn');
|
||||
const ind = document.getElementById('rec-indicator');
|
||||
|
||||
if (!isRecording) {
|
||||
recordedChunks = [];
|
||||
const mime = getBestMimeType();
|
||||
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 = saveRecording;
|
||||
mediaRecorder.start(1000);
|
||||
isRecording = true;
|
||||
recSeconds = 0;
|
||||
ind.style.display = 'flex';
|
||||
btn.textContent = '▮▮ SAVE';
|
||||
btn.innerHTML = '▮▮ SAVE';
|
||||
btn.classList.add('danger');
|
||||
recInterval = setInterval(() => {
|
||||
recSeconds++;
|
||||
const m = pad(Math.floor(recSeconds/60)), s = pad(recSeconds%60);
|
||||
document.getElementById('rec-timer').textContent = m+':'+s;
|
||||
}, 1000);
|
||||
setStatus('Recording...');
|
||||
} else {
|
||||
mediaRecorder.stop();
|
||||
isRecording = false;
|
||||
clearInterval(recInterval);
|
||||
ind.style.display = 'none';
|
||||
btn.innerHTML = '● RECORD';
|
||||
btn.classList.remove('danger');
|
||||
setStatus('Saving...');
|
||||
}
|
||||
}
|
||||
|
||||
async function startDashcam() {
|
||||
const btn = document.getElementById('start-btn');
|
||||
btn.disabled = true; btn.textContent = '...';
|
||||
@@ -238,6 +296,7 @@ async function startDashcam() {
|
||||
btn.textContent = 'RESTART'; btn.disabled = false;
|
||||
if (watchId !== null) { navigator.geolocation.clearWatch(watchId); watchId = null; }
|
||||
maxSpeed = 0; totalDist = 0; lastPos = null;
|
||||
currentGPS = { lat: null, lng: null, alt: null, acc: null, hdg: null, speed: 0 };
|
||||
startGPS();
|
||||
if (clockInterval) clearInterval(clockInterval);
|
||||
clockInterval = setInterval(updateClock, 1000);
|
||||
|
||||
Reference in New Issue
Block a user