Files
Rivertalk/assets/js/hooks/audio_recorder.js
T
2026-02-12 17:10:03 -06:00

170 lines
6.1 KiB
JavaScript

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 });
}
}