2 Commits

Author SHA1 Message Date
43a8b98a6a update 2025-12-25 07:21:45 +06:00
0f9a7e39ce update 2025-12-25 07:21:30 +06:00
9 changed files with 136 additions and 99 deletions

View File

@@ -21,6 +21,7 @@ declare module 'vue' {
PrimePanel: typeof import('primevue/panel')['default']
PrimePassword: typeof import('primevue/password')['default']
PrimeProgressBar: typeof import('primevue/progressbar')['default']
PrimeScrollPanel: typeof import('primevue/scrollpanel')['default']
PrimeSelect: typeof import('primevue/select')['default']
PrimeSelectButton: typeof import('primevue/selectbutton')['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 stream = new MediaStream()
const audioEl = new Audio()
const sourceNode = shallowRef<MediaStreamAudioSourceNode>()
const gainNode = ctx.createGain()
let hackExecuted = false
watch(audioTrack, (track, prevTrack) => {
watch(audioTrack, async (track, prevTrack) => {
if (prevTrack)
stream.removeTrack(prevTrack)
@@ -19,16 +18,14 @@ export default function useAudioContext(audioTrack: Ref<MediaStreamTrack | undef
stream.addTrack(track)
if (!hackExecuted) {
const audioEl = new Audio()
if (!audioEl.srcObject) {
audioEl.srcObject = stream
audioEl.muted = true
hackExecuted = true
}
sourceNode.value = ctx.createMediaStreamSource(stream)
connect()
await connect()
}, { immediate: true })
useEventListener(document, 'click', async () => {
@@ -36,10 +33,16 @@ export default function useAudioContext(audioTrack: Ref<MediaStreamTrack | undef
await ctx.resume()
}
connect()
await connect()
}, { once: true })
function connect() {
onScopeDispose(() => {
audioEl.pause()
audioEl.srcObject = null
ctx.close()
})
async function connect() {
if (!sourceNode.value || ctx.state === 'suspended')
return

View File

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

View File

@@ -16,14 +16,24 @@ export const usePreferences = createGlobalState(() => {
audioOutputs,
} = 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 {
inputDeviceId,
outputDeviceId,
autoGainControl,
noiseSuppression,
echoCancellation,
videoInputs,
audioInputs,
audioOutputs,
inputDeviceExist,
outputDeviceExist,
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>
<div class="grid grid-cols-2 h-screen">
<div class="flex flex-col shadow-xl shadow-surface-950 overflow-y-hidden">
<div class="grid grid-cols-2 gap-2 p-2 h-screen grid-rows-[auto_1fr]">
<div
class="flex items-center justify-between gap-2 border-b-2 border-surface-800 px-3 py-3 bg-surface-950"
style="height: 75px;"
class="flex items-center justify-between gap-2 rounded-xl p-3 bg-surface-950"
>
<div class="inline-flex items-center gap-3">
<PrimeBadge v-if="isTauri" class="opacity-50" severity="secondary" :value="version" size="small" />
@@ -24,15 +22,8 @@
</PrimeButtonGroup>
</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
class="flex items-center justify-center gap-2 border-b-2 border-surface-800 px-3 py-3 bg-surface-900"
style="height: 75px;"
class="flex items-center justify-center rounded-xl p-3 bg-surface-950"
>
<PrimeSelectButton
v-model="activeTab"
@@ -47,8 +38,17 @@
</PrimeSelectButton>
</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 />
</div>
</PrimeScrollPanel>
</div>
</template>

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "chad",
"version": "0.2.11",
"version": "0.2.12",
"identifier": "xyz.koptilnya.chad",
"build": {
"frontendDist": "../.output/public",