This commit is contained in:
2025-12-25 07:21:30 +06:00
parent 8265e2d719
commit 0f9a7e39ce
8 changed files with 135 additions and 98 deletions

View File

@@ -21,6 +21,7 @@ declare module 'vue' {
PrimePanel: typeof import('primevue/panel')['default'] PrimePanel: typeof import('primevue/panel')['default']
PrimePassword: typeof import('primevue/password')['default'] PrimePassword: typeof import('primevue/password')['default']
PrimeProgressBar: typeof import('primevue/progressbar')['default'] PrimeProgressBar: typeof import('primevue/progressbar')['default']
PrimeScrollPanel: typeof import('primevue/scrollpanel')['default']
PrimeSelect: typeof import('primevue/select')['default'] PrimeSelect: typeof import('primevue/select')['default']
PrimeSelectButton: typeof import('primevue/selectbutton')['default'] PrimeSelectButton: typeof import('primevue/selectbutton')['default']
PrimeSlider: typeof import('primevue/slider')['default'] PrimeSlider: typeof import('primevue/slider')['default']

View File

@@ -4,13 +4,12 @@ export default function useAudioContext(audioTrack: Ref<MediaStreamTrack | undef
const ctx = new (window.AudioContext || window.webkitAudioContext)() const ctx = new (window.AudioContext || window.webkitAudioContext)()
const stream = new MediaStream() const stream = new MediaStream()
const audioEl = new Audio()
const sourceNode = shallowRef<MediaStreamAudioSourceNode>() const sourceNode = shallowRef<MediaStreamAudioSourceNode>()
const gainNode = ctx.createGain() const gainNode = ctx.createGain()
let hackExecuted = false watch(audioTrack, async (track, prevTrack) => {
watch(audioTrack, (track, prevTrack) => {
if (prevTrack) if (prevTrack)
stream.removeTrack(prevTrack) stream.removeTrack(prevTrack)
@@ -19,16 +18,14 @@ export default function useAudioContext(audioTrack: Ref<MediaStreamTrack | undef
stream.addTrack(track) stream.addTrack(track)
if (!hackExecuted) { if (!audioEl.srcObject) {
const audioEl = new Audio()
audioEl.srcObject = stream audioEl.srcObject = stream
audioEl.muted = true audioEl.muted = true
hackExecuted = true
} }
sourceNode.value = ctx.createMediaStreamSource(stream) sourceNode.value = ctx.createMediaStreamSource(stream)
connect() await connect()
}, { immediate: true }) }, { immediate: true })
useEventListener(document, 'click', async () => { useEventListener(document, 'click', async () => {
@@ -36,10 +33,16 @@ export default function useAudioContext(audioTrack: Ref<MediaStreamTrack | undef
await ctx.resume() await ctx.resume()
} }
connect() await connect()
}, { once: true }) }, { once: true })
function connect() { onScopeDispose(() => {
audioEl.pause()
audioEl.srcObject = null
ctx.close()
})
async function connect() {
if (!sourceNode.value || ctx.state === 'suspended') if (!sourceNode.value || ctx.state === 'suspended')
return return

View File

@@ -165,8 +165,6 @@ export const useMediasoup = createSharedComposable(() => {
if (producerPaused) if (producerPaused)
consumer.pause() consumer.pause()
console.log('newConsumer', consumer.paused)
consumer.on('transportclose', () => { consumer.on('transportclose', () => {
if (consumers.value.delete(consumer.id)) if (consumers.value.delete(consumer.id))
triggerRef(consumers) triggerRef(consumers)
@@ -248,12 +246,13 @@ export const useMediasoup = createSharedComposable(() => {
const stream = await navigator.mediaDevices.getUserMedia({ const stream = await navigator.mediaDevices.getUserMedia({
audio: { audio: {
autoGainControl: false, deviceId: { exact: preferences.inputDeviceId.value },
noiseSuppression: true, autoGainControl: { exact: preferences.autoGainControl.value },
echoCancellation: false, echoCancellation: { exact: preferences.echoCancellation.value },
channelCount: 2, noiseSuppression: { exact: preferences.noiseSuppression.value },
}, },
}) })
const track = stream.getAudioTracks()[0] const track = stream.getAudioTracks()[0]
if (!track) if (!track)
@@ -270,6 +269,7 @@ export const useMediasoup = createSharedComposable(() => {
producers.value.set(micProducer.value.id, micProducer.value) producers.value.set(micProducer.value.id, micProducer.value)
triggerRef(producers) triggerRef(producers)
triggerRef(micProducer)
micProducer.value.on('transportclose', () => { micProducer.value.on('transportclose', () => {
micProducer.value = undefined micProducer.value = undefined
@@ -297,6 +297,7 @@ export const useMediasoup = createSharedComposable(() => {
} }
finally { finally {
triggerRef(producers) triggerRef(producers)
triggerRef(micProducer)
} }
micProducer.value = undefined micProducer.value = undefined
@@ -370,6 +371,32 @@ export const useMediasoup = createSharedComposable(() => {
} }
} }
watch(
preferences.inputDeviceId,
async (inputDeviceId) => {
await disableMic()
if (!inputDeviceId)
return
await enableMic()
},
)
watch([
preferences.inputDeviceId,
preferences.echoCancellation,
preferences.autoGainControl,
preferences.noiseSuppression,
], async ([inputDeviceId]) => {
await disableMic()
if (!inputDeviceId)
return
await enableMic()
})
return { return {
init, init,
consumers, consumers,

View File

@@ -16,14 +16,24 @@ export const usePreferences = createGlobalState(() => {
audioOutputs, audioOutputs,
} = useDevicesList() } = useDevicesList()
const inputDeviceExist = computed(() => {
return audioInputs.value.some(device => device.deviceId === inputDeviceId.value)
})
const outputDeviceExist = computed(() => {
return audioOutputs.value.some(device => device.deviceId === outputDeviceId.value)
})
return { return {
inputDeviceId, inputDeviceId,
outputDeviceId, outputDeviceId,
autoGainControl, autoGainControl,
noiseSuppression, noiseSuppression,
echoCancellation, echoCancellation,
videoInputs, inputDeviceExist,
audioInputs, outputDeviceExist,
audioOutputs, videoInputs: computed(() => JSON.parse(JSON.stringify(videoInputs.value))),
audioInputs: computed(() => JSON.parse(JSON.stringify(audioInputs.value))),
audioOutputs: computed(() => JSON.parse(JSON.stringify(audioOutputs.value))),
} }
}) })

View File

@@ -1,9 +1,7 @@
<template> <template>
<div class="grid grid-cols-2 h-screen"> <div class="grid grid-cols-2 gap-2 p-2 h-screen grid-rows-[auto_1fr]">
<div class="flex flex-col shadow-xl shadow-surface-950 overflow-y-hidden">
<div <div
class="flex items-center justify-between gap-2 border-b-2 border-surface-800 px-3 py-3 bg-surface-950" class="flex items-center justify-between gap-2 rounded-xl p-3 bg-surface-950"
style="height: 75px;"
> >
<div class="inline-flex items-center gap-3"> <div class="inline-flex items-center gap-3">
<PrimeBadge v-if="isTauri" class="opacity-50" severity="secondary" :value="version" size="small" /> <PrimeBadge v-if="isTauri" class="opacity-50" severity="secondary" :value="version" size="small" />
@@ -24,15 +22,8 @@
</PrimeButtonGroup> </PrimeButtonGroup>
</div> </div>
<div v-auto-animate class="p-3 overflow-y-auto flex-1 bg-surface-900 overflow-hidden divide-y divide-surface-800">
<ClientRow v-for="client of clients" :key="client.id" :client="client" />
</div>
</div>
<div class="bg-surface-950 overflow-y-auto">
<div <div
class="flex items-center justify-center gap-2 border-b-2 border-surface-800 px-3 py-3 bg-surface-900" class="flex items-center justify-center rounded-xl p-3 bg-surface-950"
style="height: 75px;"
> >
<PrimeSelectButton <PrimeSelectButton
v-model="activeTab" v-model="activeTab"
@@ -47,8 +38,17 @@
</PrimeSelectButton> </PrimeSelectButton>
</div> </div>
<PrimeScrollPanel class="bg-surface-900 rounded-xl" style="min-height: 0">
<div v-auto-animate class="p-3 divide-y divide-surface-800">
<ClientRow v-for="client of clients" :key="client.id" :client="client" />
</div>
</PrimeScrollPanel>
<PrimeScrollPanel class="bg-surface-900 rounded-xl" style="min-height: 0">
<div class="p-3">
<slot /> <slot />
</div> </div>
</PrimeScrollPanel>
</div> </div>
</template> </template>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="flex items-center justify-center p-3"> <div class="flex items-center justify-center">
<PrimeCard> <PrimeCard>
<template #content> <template #content>
The chat is under development. The chat is under development.
@@ -9,9 +9,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { clients } = useClients()
const { producers, consumers } = useMediasoup()
definePageMeta({ definePageMeta({
name: 'Index', name: 'Index',
}) })

View File

@@ -1,13 +1,14 @@
<template> <template>
<div class="px-3 py-6"> <div>
<PrimeFloatLabel variant="on"> <PrimeFloatLabel variant="on">
<PrimeSelect <PrimeSelect
v-model="inputDeviceId" v-model="inputDeviceId"
:options="audioInputsHack" :options="audioInputs"
option-label="label" option-label="label"
option-value="deviceId" option-value="deviceId"
fluid fluid
input-id="inputDevice" input-id="inputDevice"
:invalid="!inputDeviceExist"
/> />
<label for="inputDevice">Input device</label> <label for="inputDevice">Input device</label>
</PrimeFloatLabel> </PrimeFloatLabel>
@@ -27,23 +28,25 @@
<label for="noiseSuppression">Noise Suppression</label> <label for="noiseSuppression">Noise Suppression</label>
</div> </div>
<PrimeFloatLabel variant="on"> <!-- <PrimeFloatLabel variant="on"> -->
<PrimeSelect <!-- <PrimeSelect -->
v-model="outputDeviceId" <!-- v-model="outputDeviceId" -->
:options="audioOutputsHack" <!-- :options="audioOutputs" -->
option-label="label" <!-- option-label="label" -->
option-value="deviceId" <!-- option-value="deviceId" -->
fluid <!-- fluid -->
class="mt-6" <!-- class="mt-6" -->
input-id="outputDevice" <!-- input-id="outputDevice" -->
/> <!-- :invalid="!outputDeviceExist" -->
<label for="outputDevice">Output device</label> <!-- -->
</PrimeFloatLabel> <!-- /> -->
<!-- <label for="outputDevice">Output device</label> -->
<!-- </PrimeFloatLabel> -->
<template v-if="isTauri">
<PrimeDivider /> <PrimeDivider />
<PrimeButton <PrimeButton
v-if="isTauri"
size="small" size="small"
label="Check for Updates" label="Check for Updates"
fluid fluid
@@ -51,6 +54,7 @@
:loading="checking" :loading="checking"
@click="onCheckForUpdates" @click="onCheckForUpdates"
/> />
</template>
</div> </div>
<PrimeToast position="bottom-center" group="updater"> <PrimeToast position="bottom-center" group="updater">
@@ -80,19 +84,14 @@ const {
autoGainControl, autoGainControl,
noiseSuppression, noiseSuppression,
echoCancellation, echoCancellation,
inputDeviceExist,
outputDeviceExist,
audioInputs, audioInputs,
audioOutputs, audioOutputs,
} = usePreferences() } = usePreferences()
const toast = useToast() const toast = useToast()
const audioInputsHack = computed(() => {
return JSON.parse(JSON.stringify(audioInputs.value))
})
const audioOutputsHack = computed(() => {
return JSON.parse(JSON.stringify(audioOutputs.value))
})
async function onCheckForUpdates() { async function onCheckForUpdates() {
const update = await checkForUpdates() const update = await checkForUpdates()

View File

@@ -1,5 +1,5 @@
<template> <template>
<form class="px-3 py-6" @submit.prevent="save()"> <form @submit.prevent="save()">
<PrimeFloatLabel variant="on"> <PrimeFloatLabel variant="on">
<PrimeInputText id="displayName" v-model="displayName" fluid autocomplete="off" /> <PrimeInputText id="displayName" v-model="displayName" fluid autocomplete="off" />
<label for="displayName">Display name</label> <label for="displayName">Display name</label>