975c7a2e40
Record short E2E-encrypted audio attachments from the live log, link them to events via __live:voice markers, and play them back in the stream and chronological event table. Co-authored-by: Cursor <cursoragent@cursor.com>
40 lines
1.1 KiB
TypeScript
40 lines
1.1 KiB
TypeScript
export const VOICE_MEMO_MAX_DURATION_SEC = 60
|
|
export const VOICE_MEMO_MAX_BLOB_BYTES = 800_000
|
|
|
|
const MIME_CANDIDATES = [
|
|
'audio/webm;codecs=opus',
|
|
'audio/webm',
|
|
'audio/mp4',
|
|
'audio/ogg;codecs=opus'
|
|
]
|
|
|
|
export function pickMediaRecorderMimeType(): string | undefined {
|
|
if (typeof MediaRecorder === 'undefined') return undefined
|
|
for (const mime of MIME_CANDIDATES) {
|
|
if (MediaRecorder.isTypeSupported(mime)) return mime
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
export function blobToAudioDataUrl(blob: Blob): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader()
|
|
reader.onload = () => resolve(String(reader.result))
|
|
reader.onerror = () => reject(new Error('audio_read_failed'))
|
|
reader.readAsDataURL(blob)
|
|
})
|
|
}
|
|
|
|
export function formatVoiceDuration(seconds: number): string {
|
|
const s = Math.max(0, Math.floor(seconds))
|
|
const m = Math.floor(s / 60)
|
|
const r = s % 60
|
|
return `${m}:${String(r).padStart(2, '0')}`
|
|
}
|
|
|
|
export function assertVoiceMemoBlobSize(blob: Blob): void {
|
|
if (blob.size > VOICE_MEMO_MAX_BLOB_BYTES) {
|
|
throw new Error('VOICE_MEMO_TOO_LARGE')
|
|
}
|
|
}
|