Skip to content

Instantly share code, notes, and snippets.

@ededejr
Created May 31, 2024 21:13
Show Gist options
  • Save ededejr/f7f3268df5e689d1c86bb66de984ce75 to your computer and use it in GitHub Desktop.
Save ededejr/f7f3268df5e689d1c86bb66de984ce75 to your computer and use it in GitHub Desktop.
record input from mic to zustand
'use client';
import { create } from 'zustand';
type RecordingStore = {
recorder?: MediaRecorder;
isRecording: boolean;
isDiscarding: boolean;
recordingTimeMs: number;
recordingProgressToMax: number;
recordingTimer?: NodeJS.Timeout;
recordingChunks: Blob[];
blobUrl?: string;
};
export const MAX_RECORDING_TIME_MS = 1000 * 60 * 30;
export const useRecordingStore = create<RecordingStore>(() => ({
recorder: undefined,
isRecording: false,
isDiscarding: false,
recordingTimeMs: 0,
recordingProgressToMax: 0,
recordingChunks: [],
blobUrl: undefined,
}));
export function useIsRecording() {
return useRecordingStore((state) => state.isRecording);
}
export function useCanSaveOrDiscardRecording() {
const isRecording = useIsRecording();
const chunks = useRecordingChunks();
const canSaveOrDiscard = Boolean(chunks.length && !isRecording);
return canSaveOrDiscard;
}
export function useRecordingTimeMs() {
return useRecordingStore((state) => state.recordingTimeMs);
}
export function useRecordingChunks() {
return useRecordingStore((state) => state.recordingChunks);
}
export function useRecordingProgressToMax() {
return useRecordingStore((state) => state.recordingProgressToMax);
}
export function setRecorder(recorder: MediaRecorder) {
useRecordingStore.setState({ recorder });
}
export function setIsRecording(isRecording: boolean) {
useRecordingStore.setState({ isRecording });
}
export function setRecordingChunk(chunk: Blob) {
useRecordingStore.setState((state) => {
state.recordingChunks.push(chunk);
return state;
});
}
export function setRecordingTimeMs(timeMs: number) {
useRecordingStore.setState({
recordingTimeMs: timeMs,
recordingProgressToMax: (timeMs / MAX_RECORDING_TIME_MS) * 100,
});
}
export function setBlobUrl(blobUrl: string) {
useRecordingStore.setState({ blobUrl });
}
export async function startRecording() {
let recorder = useRecordingStore.getState().recorder;
if (!recorder) {
recorder = await createRecorder();
}
if (recorder.state === 'paused') {
recorder.resume();
} else {
recorder.start(1000);
}
setIsRecording(true);
const recordingTimer = setInterval(() => {
const { recordingTimeMs } = useRecordingStore.getState();
setRecordingTimeMs(recordingTimeMs + 1000);
}, 1000);
useRecordingStore.setState({ recordingTimer });
}
export function pauseRecording() {
const { recorder } = useRecordingStore.getState();
if (recorder) {
recorder.pause();
}
}
export function endRecording() {
const { recorder } = useRecordingStore.getState();
if (recorder) {
recorder.stop();
}
}
export function discardRecording() {
const { recorder, recordingTimer } = useRecordingStore.getState();
if (recorder) {
clearInterval(recordingTimer);
useRecordingStore.setState({
isRecording: false,
isDiscarding: true,
recordingTimer: undefined,
recordingTimeMs: 0,
recordingProgressToMax: 0,
recordingChunks: [],
blobUrl: undefined,
});
}
}
export async function createRecorder() {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: false,
});
const recorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus',
});
recorder.ondataavailable = function (e) {
// could also stream to server from here
if (e.data.size > 0) {
setRecordingChunk(e.data);
}
};
recorder.onpause = function () {
const { recordingTimer } = useRecordingStore.getState();
setIsRecording(false);
clearInterval(recordingTimer);
};
recorder.onstop = async function () {
const { recordingChunks, isDiscarding } = useRecordingStore.getState();
stream.getAudioTracks().forEach((track) => track.stop());
if (isDiscarding) {
useRecordingStore.setState({ isDiscarding: false });
return;
}
const blobType = 'audio/webm;codecs=opus';
const blob = new Blob(recordingChunks, { type: blobType });
setBlobUrl(URL.createObjectURL(blob));
try {
const formData = new FormData();
const fileName = `${Date.now()}-recording.webm`;
formData.append('files', blob, fileName);
// @todo
// at this point:
// 1. send to server
// 2. save for playback maybe?
await fetch('/api/upload', {
method: 'POST',
body: formData,
});
} catch (error) {
window.alert('Something went wrong uploading the recording');
console.error(error);
}
};
useRecordingStore.setState({ recorder });
return recorder;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment