-
-
-
@@ -81,9 +74,7 @@
DIST0.00km
-
-
@@ -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);