From b87a376f13f4d89037aca87d3685057e9f9107f6 Mon Sep 17 00:00:00 2001 From: Simone Civetta Date: Mon, 12 Jan 2026 10:55:59 +0100 Subject: [PATCH 01/10] Add separate audio tracks, checkpoint system, and security fixes Features: - Save host and guest audio tracks separately (-host.webm, -guest.webm) - Auto-save checkpoints every 60 seconds to IndexedDB - Checkpoint management UI (view, download, delete) - Improved browser close confirmation during recording - XSS vulnerability fix in checkpoint modal Co-Authored-By: Claude --- index.html | 332 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 302 insertions(+), 30 deletions(-) diff --git a/index.html b/index.html index ebc7f6a..09b2809 100644 --- a/index.html +++ b/index.html @@ -40,6 +40,7 @@

Status:

local
sec delay
remote
+ @@ -152,14 +153,27 @@ var localStream; var dcomp; var recorder; + var recorderHost; + var recorderGuest; var chunks = []; + var hostChunks = []; + var guestChunks = []; var saved = false; + var hostSaved = false; + var guestSaved = false; var startRecTime = 0; var lastLoss = 0; var lastRecv =0; var softstereo = true; var join; + // Checkpoint system + var sessionId; + var checkpointInterval; + var lastCheckpointTime = 0; + var checkpointDB; + const CHECKPOINT_INTERVAL_MS = 60000; // 60 seconds + var mute = false; var peerConnectionOfferAnswerCriteria = {offerToReceiveAudio: true, offerToReceiveVideo: false }; @@ -284,8 +298,8 @@ }) .catch(e => console.log("offer not created due to ", e) ); } - function saveData(blob) { - var fileName = 'distributedFuture-'+new Date().toISOString() + '.webm'; + function saveData(blob, suffix) { + var fileName = 'distributedFuture-' + new Date().toISOString() + '-' + suffix + '.webm'; console.log("Save data ?"); var a = document.createElement("a"); @@ -297,22 +311,219 @@ a.download = fileName; a.click(); window.URL.revokeObjectURL(url); - saved = true; - $("#status").text("Call saved as "+fileName); + return fileName; + } + + // IndexedDB Checkpoint functions + function initCheckpointDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open('PodCallCheckpoints', 1); + request.onerror = () => reject('IndexedDB error'); + request.onsuccess = (e) => { + checkpointDB = e.target.result; + resolve(checkpointDB); + }; + request.onupgradeneeded = (e) => { + const db = e.target.result; + if (!db.objectStoreNames.contains('checkpoints')) { + const store = db.createObjectStore('checkpoints', { keyPath: 'id', autoIncrement: true }); + store.createIndex('sessionId', 'sessionId', { unique: false }); + store.createIndex('timestamp', 'timestamp', { unique: false }); + } + }; + }); + } + + function saveCheckpoint(hostBlob, guestBlob, duration) { + if (!checkpointDB) return Promise.reject('DB not initialized'); + + return new Promise((resolve, reject) => { + const transaction = checkpointDB.transaction(['checkpoints'], 'readwrite'); + const store = transaction.objectStore('checkpoints'); + const index = store.index('sessionId'); + + // Delete previous checkpoint for this session + const deleteReq = index.openCursor(IDBKeyRange.only(sessionId)); + deleteReq.onsuccess = (e) => { + const cursor = e.target.result; + if (cursor) { + cursor.delete(); + cursor.continue(); + } + }; + + // Add new checkpoint + const checkpoint = { + sessionId: sessionId, + timestamp: Date.now(), + hostBlob: hostBlob, + guestBlob: guestBlob, + duration: duration + }; + const addReq = store.add(checkpoint); + addReq.onsuccess = () => { + console.log('Checkpoint saved'); + resolve(); + }; + addReq.onerror = () => reject('Failed to save checkpoint'); + }); + } + + function getCheckpoints() { + return new Promise((resolve, reject) => { + if (!checkpointDB) return reject('DB not initialized'); + const transaction = checkpointDB.transaction(['checkpoints'], 'readonly'); + const store = transaction.objectStore('checkpoints'); + const request = store.getAll(); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject('Failed to get checkpoints'); + }); + } + + function deleteCheckpoint(id) { + return new Promise((resolve, reject) => { + if (!checkpointDB) return reject('DB not initialized'); + const transaction = checkpointDB.transaction(['checkpoints'], 'readwrite'); + const store = transaction.objectStore('checkpoints'); + const request = store.delete(id); + request.onsuccess = () => resolve(); + request.onerror = () => reject('Failed to delete checkpoint'); + }); + } + + function downloadBlob(blob, suffix) { + var a = document.createElement("a"); + document.body.appendChild(a); + a.style = "display: none"; + var url = window.URL.createObjectURL(blob); + a.href = url; + a.download = 'distributedFuture-' + new Date(blob.timestamp || Date.now()).toISOString() + '-' + suffix + '.webm'; + a.click(); + window.URL.revokeObjectURL(url); + a.remove(); + } + // HTML escape function to prevent XSS + function escapeHtml(unsafe) { + if (typeof unsafe !== 'string') return unsafe; + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); } function stopCall(){ localStream.getAudioTracks()[0].stop(); + + // Clear checkpoint interval + if (checkpointInterval) { + clearInterval(checkpointInterval); + } + if (initiator) { - recorder.stop(); + $("#status").text("Saving recordings..."); + // Stop both recorders + if (recorderHost && recorderHost.state !== 'inactive') { + recorderHost.stop(); + } + if (recorderGuest && recorderGuest.state !== 'inactive') { + recorderGuest.stop(); + } } else { clearInterval(recorder); } -// save file here.... + $("#status").text("Call ended."); + // Show checkpoints modal + showCheckpointsModal(); + } + + function showCheckpointsModal() { + getCheckpoints().then(checkpoints => { + var modalHtml = ''; + + // Remove existing modal if present + $('#checkpointsModal').remove(); + + // Add new modal + $('body').append(modalHtml); + + // Bind click events + $('.download-host').click(function() { + var id = parseInt($(this).data('id')); + var cp = checkpoints.find(c => c.id === id); + if (cp) downloadBlob(cp.hostBlob, 'host'); + }); + + $('.download-guest').click(function() { + var id = parseInt($(this).data('id')); + var cp = checkpoints.find(c => c.id === id); + if (cp) downloadBlob(cp.guestBlob, 'guest'); + }); + $('.delete-checkpoint').click(function() { + var id = parseInt($(this).data('id')); + deleteCheckpoint(id).then(() => { + $('#checkpointsModal').modal('hide'); + setTimeout(showCheckpointsModal, 300); + }); + }); + + $('#checkpointsModal').modal('show'); + }).catch(err => { + console.error('Failed to load checkpoints:', err); + }); } @@ -385,35 +596,90 @@ if (kind.indexOf("audio")!= -1) { document.getElementById("them").srcObject = stream; var peer = myac.createMediaStreamSource(stream); + + // Generate session ID for this recording + sessionId = 'session-' + Date.now(); + if (initiator) { - if (softstereo){ - let panyou = myac.createStereoPanner(); - panyou.pan.value = +0.3; - peer.connect(panyou) - panyou.connect(dcomp); - } else { - let splityou = myac.createChannelSplitter(2); // 2 outputs L and R - peer.connect(splityou); - splityou.connect(join, 1, 1); - // join already connected to dcomp - } + // Create separate recording destinations for host and guest tracks + var recStreamHost = myac.createMediaStreamDestination(); + var recStreamGuest = myac.createMediaStreamDestination(); + + // Connect guest (remote) audio to guest recorder + peer.connect(recStreamGuest); + + // Setup host recorder (local microphone) + // The local stream source is already connected to dcomp in setupAudio + // We need to also connect it to recStreamHost + var localNode = myac.createMediaStreamSource(localStream); + localNode.connect(recStreamHost); + + // Create host recorder + recorderHost = new MediaRecorder(recStreamHost.stream); + recorderHost.ondataavailable = function(evt) { + hostChunks.push(evt.data); + repaintDuration(); + }; + recorderHost.onstop = function(evt) { + var blob = new Blob(hostChunks, { 'type' : 'audio/webm; codecs=opus' }); + var fileName = saveData(blob, 'host'); + hostSaved = true; + console.log("Host track saved: " + fileName); + + // Save as final checkpoint + var duration = Date.now() - startRecTime; + if (guestSaved) { + saveCheckpoint(blob, new Blob(guestChunks, { 'type' : 'audio/webm; codecs=opus' }), duration); + } + }; - var recStream = myac.createMediaStreamDestination(); - recorder = new MediaRecorder(recStream.stream); - dcomp.connect(recStream); - recorder.ondataavailable = function(evt) { - chunks.push(evt.data); + // Create guest recorder + recorderGuest = new MediaRecorder(recStreamGuest.stream); + recorderGuest.ondataavailable = function(evt) { + guestChunks.push(evt.data); repaintDuration(); }; - recorder.onstop = function(evt) { - // Make blob out of our blobs, and open it. - var blob = new Blob(chunks, { 'type' : 'audio/ogg; codecs=opus' }); - saveData(blob) + recorderGuest.onstop = function(evt) { + var blob = new Blob(guestChunks, { 'type' : 'audio/webm; codecs=opus' }); + var fileName = saveData(blob, 'guest'); + guestSaved = true; + console.log("Guest track saved: " + fileName); + + // Save as final checkpoint + var duration = Date.now() - startRecTime; + if (hostSaved) { + saveCheckpoint(new Blob(hostChunks, { 'type' : 'audio/webm; codecs=opus' }), blob, duration); + } }; + stream.onremovetrack = function(event) { console.log( "Removed track : " + event.track.kind + ": " + event.track.label); }; - recorder.start(10000); + + // Start both recorders + recorderHost.start(10000); + recorderGuest.start(10000); + + // Set up checkpoint interval (every 60 seconds) + checkpointInterval = setInterval(function() { + if (hostChunks.length > 0 || guestChunks.length > 0) { + var duration = Date.now() - startRecTime; + var hostBlob = new Blob(hostChunks, { 'type' : 'audio/webm; codecs=opus' }); + var guestBlob = new Blob(guestChunks, { 'type' : 'audio/webm; codecs=opus' }); + saveCheckpoint(hostBlob, guestBlob, duration); + console.log("Checkpoint saved at " + duration + "ms"); + $("#status").text("Recording... Checkpoint saved (" + Math.floor(duration/60000) + ":" + String(Math.floor((duration%60000)/1000)).padStart(2,'0') + ")"); + } + }, CHECKPOINT_INTERVAL_MS); + + // Set onbeforeunload immediately when recording starts + window.onbeforeunload = function() { + if (!hostSaved || !guestSaved) { + return "Recording in progress. If you leave this page, you will lose the audio files."; + } + return null; + }; + } else { recorder = window.setInterval(repaintDuration,10000); } @@ -488,9 +754,6 @@ } var act = $("#stopCall"); act.click( _ => stopCall() ); - window.onbeforeunload = function() { - return !saved ? "If you leave this page you will end the interview." : null; - } } }; } @@ -695,6 +958,15 @@ $("#chosenAction").hide(); $("#packetLoss").hide(); $("#statsZone").hide(); + + // Initialize checkpoint database + initCheckpointDB().then(() => { + console.log("Checkpoint DB initialized"); + $("#viewCheckpoints").click(showCheckpointsModal); + }).catch(err => { + console.error("Failed to initialize checkpoint DB:", err); + }); + if (isFacebookApp()){ $("#status").text("Facebook apps block webrtc in their 'browser' "); $("#facebookapp").show(); From a3ab7a2edf686ec171750dbb3b63b127de820abd Mon Sep 17 00:00:00 2001 From: Simone Civetta Date: Mon, 12 Jan 2026 11:34:41 +0100 Subject: [PATCH 02/10] Fix UI: add margins, padding, and button spacing - Add proper body padding - Add spacing between buttons in chosenAction row - Add margins to containers - Style duration badge - Improve footer layout with centered text and border - Add spacing to modal content - Add margins to small buttons Co-Authored-By: Claude --- index.html | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/index.html b/index.html index 09b2809..21b8c00 100644 --- a/index.html +++ b/index.html @@ -11,6 +11,44 @@ From a773a05fc9051d9ebaaa81137a0e9bc66fece626 Mon Sep 17 00:00:00 2001 From: Simone Civetta Date: Mon, 12 Jan 2026 11:35:27 +0100 Subject: [PATCH 03/10] Add confirmation prompt when user presses stop - Add stop confirmation modal with "Are you sure you want to end the recording?" - Modify stop button to show confirmation modal instead of directly stopping - Add confirmStop button handler that actually calls stopCall() Co-Authored-By: Claude --- index.html | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 21b8c00..f2dc13a 100644 --- a/index.html +++ b/index.html @@ -97,6 +97,25 @@ +