import socket from "../user_socket" export const AudioRecorder = { mounted() { this.mediaRecorder = null; this.isRecording = false; this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); this.queue = []; this.sourceBuffer = null; this.mediaSource = null; this.liveAudio = new Audio(); this.liveAudio.autoplay = true; // Join channel this.channel = socket.channel("audio:lobby", {}) this.channel.join() .receive("ok", resp => { console.log("Joined audio lobby", resp) }) .receive("error", resp => { console.error("Unable to join audio lobby", resp) }) // Listen for start of others' audio this.channel.on("audio_start", (payload) => { this.prepareLivePlayback(); }); // Listen for incoming chunks (others talking) this.channel.on("audio_chunk", (payload) => { this.queueChunk(payload.data) }) // UI Events const btn = this.el.querySelector("button#record-btn"); btn.addEventListener("click", (e) => { e.preventDefault(); if (this.isRecording) { this.stopRecording(); } else { this.startRecording(crypto.randomUUID()); } }); // External event to play live stream (from UI button) this.el.addEventListener("play_live", () => { console.log("Play Live triggered"); console.log("MediaSource state:", this.mediaSource ? this.mediaSource.readyState : "null"); console.log("SourceBuffer state:", this.sourceBuffer ? (this.sourceBuffer.updating ? "updating" : "ready") : "null"); console.log("Buffer Queue length:", this.queue.length); if (this.liveAudio) { console.log("Attempting to play live audio..."); this.liveAudio.play() .then(() => console.log("Live audio playing started")) .catch(err => console.error("Error playing live audio", err)); } else { console.error("Live audio object not initialized"); } }); }, prepareLivePlayback() { console.log("Preparing live playback MediaSource..."); this.queue = []; this.mediaSource = new MediaSource(); this.liveAudio.src = URL.createObjectURL(this.mediaSource); this.mediaSource.addEventListener('sourceopen', () => { console.log("MediaSource open"); try { if (this.mediaSource.sourceBuffers.length > 0) return; this.sourceBuffer = this.mediaSource.addSourceBuffer('audio/webm; codecs="opus"'); this.sourceBuffer.mode = 'sequence'; this.sourceBuffer.addEventListener('updateend', () => { this.processQueue(); }); console.log("SourceBuffer added and mode set to sequence"); } catch (e) { console.error("Error adding SourceBuffer", e); } }); }, queueChunk(base64) { // console.log("Received chunk, queueing..."); const binaryString = window.atob(base64); const len = binaryString.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); } this.queue.push(bytes); this.processQueue(); }, processQueue() { if (!this.sourceBuffer) { // console.warn("No SourceBuffer available to process queue"); return; } if (this.sourceBuffer.updating) { // console.log("SourceBuffer updating, waiting..."); return; } if (this.queue.length === 0) { return; } const chunk = this.queue.shift(); try { this.sourceBuffer.appendBuffer(chunk); // console.log("Appended buffer. Queue size:", this.queue.length); } catch (e) { console.error("Error appending to SourceBuffer", e); } }, startRecording(id) { console.log("Starting recording...", id); if (this.isRecording) return; this.isRecording = true; this.currentId = id; this.startTime = Date.now(); this.el.setAttribute("data-recording", "true"); // Notify channel we are starting this.channel.push("audio_start", { id: this.currentId }); navigator.mediaDevices.getUserMedia({ audio: true }) .then(stream => { console.log("Microphone access granted"); this.mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' }); this.mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0 && this.channel) { const reader = new FileReader(); reader.onload = () => { this.channel.push("audio_chunk", reader.result); }; reader.readAsArrayBuffer(e.data); } }; this.mediaRecorder.start(100); // chunk every 100ms this.pushEvent("recording_started", { id }); }) .catch(err => { console.error("Error accessing microphone", err); this.isRecording = false; }); }, stopRecording() { console.log("Stopping recording..."); if (!this.isRecording) return; this.isRecording = false; this.el.removeAttribute("data-recording"); if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") { this.mediaRecorder.stop(); this.mediaRecorder.stream.getTracks().forEach(track => track.stop()); } const duration = Date.now() - this.startTime; this.channel.push("audio_end", { id: this.currentId, duration_ms: duration }); this.pushEvent("recording_stopped", { id: this.currentId, duration: duration }); } }