feat(stage-web): added audit recorder demo as devtools

This commit is contained in:
Neko Ayaka
2025-08-31 14:03:19 +08:00
parent 7a1bc0064b
commit 606aae2cbf
6 changed files with 608 additions and 47 deletions
+1
View File
@@ -60,6 +60,7 @@
"drizzle-orm": "^0.44.5",
"jszip": "^3.10.1",
"localforage": "^1.10.0",
"mediabunny": "^1.11.2",
"node-vibrant": "^4.0.3",
"nprogress": "^0.2.0",
"ofetch": "^1.4.1",
+1
View File
@@ -61,6 +61,7 @@
"html2canvas": "^1.4.1",
"jszip": "^3.10.1",
"localforage": "^1.10.0",
"mediabunny": "^1.11.2",
"nanoid": "^5.1.5",
"node-vibrant": "^4.0.3",
"nprogress": "^0.2.0",
@@ -1,45 +1,73 @@
<script lang="ts" setup>
import { Button } from '@proj-airi/stage-ui/components'
import { Option, Select } from '@proj-airi/ui'
import { onMounted, onUnmounted } from 'vue'
<script setup lang="ts">
import { useDevicesList, useObjectUrl } from '@vueuse/core'
import { BufferTarget, MediaStreamAudioTrackSource, Output, QUALITY_MEDIUM, WavOutputFormat } from 'mediabunny'
import { computed, ref } from 'vue'
import { useAudioInput } from '../../composables/audio-input'
import { useAudioRecord } from '../../composables/audio-record'
const { audioInputs } = useDevicesList({ constraints: { audio: true }, requestPermissions: true })
const constraintId = ref('')
const { audioInputs, selectedAudioInputId, start, stop, media, request } = useAudioInput()
const { startRecord, stopRecord } = useAudioRecord(media.stream, start)
async function getMediaStreamTrack(constraint: ConstrainDOMString) {
const stream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId: constraint } })
return stream.getAudioTracks()[0]
}
onMounted(() => request())
onUnmounted(() => stop())
let output: Output | undefined
let audioInputTrack: MediaStreamAudioTrack | undefined
let format: string | undefined
const recorded = ref<ArrayBuffer[]>([])
const recordedUrls = computed(() => recorded.value.map(rec => useObjectUrl(new Blob([rec], { type: format })).value))
async function handleStart() {
audioInputTrack = await getMediaStreamTrack(constraintId.value)
output = new Output({ format: new WavOutputFormat(), target: new BufferTarget() })
const audioSource = new MediaStreamAudioTrackSource(audioInputTrack, { codec: 'pcm-f32', bitrate: QUALITY_MEDIUM })
audioSource.errorPromise.catch(console.error)
output.addAudioTrack(audioSource)
format = await output.getMimeType()
await output.start()
}
async function handleStop() {
await output?.finalize()
const bufferTarget = output?.target as BufferTarget | undefined
bufferTarget?.buffer && recorded.value.push(bufferTarget?.buffer)
}
function handleCancel() {
output?.cancel()
}
</script>
<template>
<div>
<Select v-model="selectedAudioInputId" @change="() => start()">
<template #default="{ value }">
<div>
{{ value ? audioInputs.find(device => device.deviceId === value)?.label : 'Select Audio Input' }}
</div>
</template>
<template #options="{ hide }">
<Option
v-for="device in audioInputs"
:key="device.deviceId"
:value="device.deviceId"
:active="device.deviceId === selectedAudioInputId"
@click="hide()"
>
{{ device.label }}
</Option>
</template>
</Select>
<div class="mt-4 w-full flex justify-center gap-2">
<Button @click="startRecord">
Start Recording
</Button>
<Button @click="stopRecord">
Stop Recording
</Button>
<div>
<select v-model="constraintId">
<option value="">
Select
</option>
<option v-for="(item, index) of audioInputs" :key="index" :value="item.deviceId">
{{ item.label }}
</option>
</select>
</div>
<div space-x-2>
<button @click="handleStart">
Start
</button>
<button @click="handleCancel">
Cancel
</button>
<button @click="handleStop">
Stop
</button>
</div>
<div>
<audio v-for="(url, index) in recordedUrls" :key="index" controls>
<source :src="url" type="audio/wav">
</audio>
</div>
</div>
</template>