From a084f168669ca2ab24122559d34b9ebe5bf6c6c1 Mon Sep 17 00:00:00 2001 From: "kushal.saha" Date: Thu, 6 Aug 2026 12:47:10 +0000 Subject: [PATCH] fix: multiple audio track issue --- resources/views/interview/index.blade.php | 60 ++++++++++++++++++----- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/resources/views/interview/index.blade.php b/resources/views/interview/index.blade.php index 6e907ea..ca8b6a0 100644 --- a/resources/views/interview/index.blade.php +++ b/resources/views/interview/index.blade.php @@ -2149,6 +2149,7 @@ function endCallForAll() { // --- SILENT CALL RECORDING ENGINE FOR INTERVIEWERS & ADMINS --- let adminMediaRecorder = null; let adminRecordedChunks = []; + let recAudioContext = null; function startCallRecording() { if (!currentInterviewId) { @@ -2160,28 +2161,59 @@ function startCallRecording() { let candidateStream = (candVid && candVid.srcObject) ? candVid.srcObject : null; let streamToRecord = new MediaStream(); - // Add live Candidate Video and Audio tracks + // 1. Add Video Track (Candidate video or Interviewer fallback) if (candidateStream) { - candidateStream.getTracks().forEach(track => { + candidateStream.getVideoTracks().forEach(track => { if (track.readyState === 'live') { try { streamToRecord.addTrack(track); } catch(e) {} } }); } - // Add Interviewer Local Microphone Audio track for 2-way call recording - if (interviewerLocalStream) { - interviewerLocalStream.getAudioTracks().forEach(track => { - if (track.readyState === 'live' && !streamToRecord.getAudioTracks().includes(track)) { + if (streamToRecord.getVideoTracks().length === 0 && interviewerLocalStream) { + interviewerLocalStream.getVideoTracks().forEach(track => { + if (track.readyState === 'live') { try { streamToRecord.addTrack(track); } catch(e) {} } }); - if (streamToRecord.getVideoTracks().length === 0) { - interviewerLocalStream.getVideoTracks().forEach(track => { - if (track.readyState === 'live') { - try { streamToRecord.addTrack(track); } catch(e) {} - } - }); + } + + // 2. Mix Multiple Audio Tracks into a SINGLE Audio Track via Web Audio API + // (MediaRecorder standard throws DOMException if streamToRecord has >1 audio track) + const candAudioTracks = candidateStream ? candidateStream.getAudioTracks().filter(t => t.readyState === 'live') : []; + const adminAudioTracks = interviewerLocalStream ? interviewerLocalStream.getAudioTracks().filter(t => t.readyState === 'live') : []; + + if (candAudioTracks.length > 0 || adminAudioTracks.length > 0) { + try { + const AudioContextClass = window.AudioContext || window.webkitAudioContext; + if (recAudioContext) { + try { recAudioContext.close(); } catch(e) {} + } + recAudioContext = new AudioContextClass(); + const audioDestination = recAudioContext.createMediaStreamDestination(); + + if (candAudioTracks.length > 0) { + const candAudioStream = new MediaStream(candAudioTracks); + const candSource = recAudioContext.createMediaStreamSource(candAudioStream); + candSource.connect(audioDestination); + } + + if (adminAudioTracks.length > 0) { + const adminAudioStream = new MediaStream(adminAudioTracks); + const adminSource = recAudioContext.createMediaStreamSource(adminAudioStream); + adminSource.connect(audioDestination); + } + + const mixedAudioTrack = audioDestination.stream.getAudioTracks()[0]; + if (mixedAudioTrack) { + streamToRecord.addTrack(mixedAudioTrack); + } + } catch(e) { + console.warn('Web Audio API mixing fallback:', e); + const fallbackAudio = candAudioTracks[0] || adminAudioTracks[0]; + if (fallbackAudio) { + try { streamToRecord.addTrack(fallbackAudio); } catch(err) {} + } } } @@ -2215,6 +2247,10 @@ function startCallRecording() { }; adminMediaRecorder.onstop = function() { + if (recAudioContext) { + try { recAudioContext.close(); } catch(e) {} + recAudioContext = null; + } if (adminRecordedChunks.length === 0) return; const recMime = adminMediaRecorder.mimeType || 'video/webm'; const isMp4 = recMime.includes('mp4');