Initial Commit
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user