170 lines
6.1 KiB
JavaScript
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 });
|
|
}
|
|
}
|