diff --git a/client/.yarn/install-state.gz b/client/.yarn/install-state.gz index 219de17..3fcd296 100644 Binary files a/client/.yarn/install-state.gz and b/client/.yarn/install-state.gz differ diff --git a/client/app/app.vue b/client/app/app.vue index 0fce194..b804a20 100644 --- a/client/app/app.vue +++ b/client/app/app.vue @@ -5,6 +5,3 @@ - - diff --git a/client/app/components/ClientRow.vue b/client/app/components/ClientRow.vue index 61dba62..6f0992b 100644 --- a/client/app/components/ClientRow.vue +++ b/client/app/components/ClientRow.vue @@ -28,7 +28,7 @@ Volume {{ volume }} - + diff --git a/client/app/composables/use-app.ts b/client/app/composables/use-app.ts index a062db4..79c20f5 100644 --- a/client/app/composables/use-app.ts +++ b/client/app/composables/use-app.ts @@ -1,12 +1,17 @@ import { createGlobalState } from '@vueuse/core' +import { useClients } from '~/composables/use-clients' export const useApp = createGlobalState(() => { + const { clients } = useClients() const mediasoup = useMediasoup() + const toast = useToast() const inputMuted = ref(false) const outputMuted = ref(false) - const me = computed(() => mediasoup.clients.value.find(client => client.isMe)) + const previousInputMuted = ref(inputMuted.value) + + const me = computed(() => clients.value.find(client => client.isMe)) function muteInput() { inputMuted.value = true @@ -38,15 +43,36 @@ export const useApp = createGlobalState(() => { muteOutput() } - watch(inputMuted, async (state) => { - if (state) + watch(inputMuted, async (inputMuted) => { + if (inputMuted) { await mediasoup.muteMic() - else + } + else { + if (outputMuted.value) { + outputMuted.value = false + } await mediasoup.unmuteMic() + } + + const toastText = inputMuted ? 'Microphone muted' : 'Microphone activated' + toast.add({ severity: 'info', summary: toastText, closable: false, life: 1000 }) + }) + + watch(outputMuted, (outputMuted) => { + if (outputMuted) { + previousInputMuted.value = inputMuted.value + muteInput() + } + else { + inputMuted.value = previousInputMuted.value + } + + const toastText = outputMuted ? 'Sound muted' : 'Sound resumed' + toast.add({ severity: 'info', summary: toastText, closable: false, life: 1000 }) }) return { - clients: mediasoup.clients, + clients, me, inputMuted, muteInput, diff --git a/client/app/composables/use-clients.ts b/client/app/composables/use-clients.ts new file mode 100644 index 0000000..dd685ec --- /dev/null +++ b/client/app/composables/use-clients.ts @@ -0,0 +1,58 @@ +import type { ChadClient, UpdatedClient } from '#shared/types' +import { createGlobalState } from '@vueuse/core' + +export const useClients = createGlobalState(() => { + const signaling = useSignaling() + const toast = useToast() + + const clients = shallowRef([]) + + watch(signaling.socket, (socket) => { + if (!socket) + return + + socket.on('clientChanged', (clientId: ChadClient['id'], updatedClient: UpdatedClient) => { + const client = getClient(clientId) + updateClient(clientId, updatedClient) + + if (updatedClient.username) + toast.add({ severity: 'info', summary: `${client?.username} is now ${updatedClient.username}`, closable: false, life: 1000 }) + }) + }) + + function getClient(clientId: ChadClient['id']) { + return clients.value.find(client => client.id === clientId) + } + + function addClient(...client: ChadClient[]) { + clients.value.push(...client) + + triggerRef(clients) + } + + function removeClient(clientId: ChadClient['id']) { + clients.value = clients.value.filter(client => client.id !== clientId) + } + + function updateClient(clientId: ChadClient['id'], updatedClient: UpdatedClient) { + const clientIdx = clients.value.findIndex(client => client.id === clientId) + + if (clientIdx === -1) + return + + clients.value[clientIdx] = { + ...clients.value[clientIdx], + ...updatedClient, + } as ChadClient + + triggerRef(clients) + } + + return { + clients, + getClient, + addClient, + removeClient, + updateClient, + } +}) diff --git a/client/app/composables/use-mediasoup.ts b/client/app/composables/use-mediasoup.ts index 9bf658c..deb80a8 100644 --- a/client/app/composables/use-mediasoup.ts +++ b/client/app/composables/use-mediasoup.ts @@ -18,8 +18,11 @@ const ICE_SERVERS: RTCIceServer[] = [ ] export const useMediasoup = createSharedComposable(() => { - const preferences = usePreferences() + const toast = useToast() + const signaling = useSignaling() + const { addClient, removeClient } = useClients() + const preferences = usePreferences() const device = shallowRef() const rtpCapabilities = shallowRef() @@ -30,8 +33,6 @@ export const useMediasoup = createSharedComposable(() => { const webcamProducer = shallowRef() const shareProducer = shallowRef() - const clients = shallowRef([]) - const consumers = shallowRef>(new Map()) const producers = shallowRef>(new Map()) @@ -121,21 +122,24 @@ export const useMediasoup = createSharedComposable(() => { }) } - clients.value = (await signaling.socket.value.emitWithAck('join', { + const joinedClients = (await signaling.socket.value.emitWithAck('join', { username: preferences.username.value, rtpCapabilities: rtpCapabilities.value, })).map(transformClient) + addClient(...joinedClients) + + toast.add({ severity: 'success', summary: 'Joined', closable: false, life: 1000 }) + await enableMic() }) socket.on('newPeer', (client) => { - clients.value.push(transformClient(client)) - triggerRef(clients) + addClient(transformClient(client)) }) socket.on('peerClosed', (id) => { - clients.value = clients.value.filter(client => client.id !== id) + removeClient(id) }) socket.on( @@ -317,7 +321,6 @@ export const useMediasoup = createSharedComposable(() => { return { init, - clients, consumers, producers, sendTransport, diff --git a/client/app/composables/use-signaling.ts b/client/app/composables/use-signaling.ts index 0a15c9b..062383a 100644 --- a/client/app/composables/use-signaling.ts +++ b/client/app/composables/use-signaling.ts @@ -3,6 +3,8 @@ import { createSharedComposable } from '@vueuse/core' import { io } from 'socket.io-client' export const useSignaling = createSharedComposable(() => { + const toast = useToast() + const socket = shallowRef() const connected = ref(false) @@ -35,6 +37,13 @@ export const useSignaling = createSharedComposable(() => { }) }, { immediate: true, flush: 'sync' }) + watch(connected, (connected) => { + if (connected) + toast.add({ severity: 'success', summary: 'Connected', closable: false, life: 1000 }) + else + toast.add({ severity: 'error', summary: 'Disconnected', closable: false, life: 1000 }) + }, { immediate: true }) + onScopeDispose(() => { socket.value?.close() }) diff --git a/client/app/layouts/default.vue b/client/app/layouts/default.vue index de9c771..f26ae77 100644 --- a/client/app/layouts/default.vue +++ b/client/app/layouts/default.vue @@ -1,6 +1,6 @@