Created
May 31, 2024 21:13
-
-
Save ededejr/f7f3268df5e689d1c86bb66de984ce75 to your computer and use it in GitHub Desktop.
record input from mic to zustand
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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