Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65583b1564 |
Binary file not shown.
2
client/app/components.d.ts
vendored
2
client/app/components.d.ts
vendored
@@ -17,12 +17,10 @@ declare module 'vue' {
|
|||||||
PrimeFloatLabel: typeof import('primevue/floatlabel')['default']
|
PrimeFloatLabel: typeof import('primevue/floatlabel')['default']
|
||||||
PrimeInputText: typeof import('primevue/inputtext')['default']
|
PrimeInputText: typeof import('primevue/inputtext')['default']
|
||||||
PrimePassword: typeof import('primevue/password')['default']
|
PrimePassword: typeof import('primevue/password')['default']
|
||||||
PrimeProgressBar: typeof import('primevue/progressbar')['default']
|
|
||||||
PrimeScrollPanel: typeof import('primevue/scrollpanel')['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']
|
||||||
PrimeTag: typeof import('primevue/tag')['default']
|
|
||||||
PrimeToast: typeof import('primevue/toast')['default']
|
PrimeToast: typeof import('primevue/toast')['default']
|
||||||
PrimeToggleSwitch: typeof import('primevue/toggleswitch')['default']
|
PrimeToggleSwitch: typeof import('primevue/toggleswitch')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
|||||||
@@ -6,29 +6,19 @@
|
|||||||
'bg-surface-800': expanded,
|
'bg-surface-800': expanded,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="p-3" @click="toggleExpand">
|
<div class="p-3 flex items-center gap-3" @click="toggleExpand">
|
||||||
<div class="flex items-center gap-3">
|
<PrimeAvatar size="small">
|
||||||
<PrimeAvatar
|
<template #icon>
|
||||||
size="small"
|
<User :size="20" />
|
||||||
class="shrink-0"
|
</template>
|
||||||
:class="{
|
</PrimeAvatar>
|
||||||
'outline-1 outline-primary outline-offset-2': speaking,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<User :size="20" />
|
|
||||||
</template>
|
|
||||||
</PrimeAvatar>
|
|
||||||
|
|
||||||
<p class="flex-1 text-sm leading-5 font-medium text-color truncate w-0">
|
<div class="flex-1 overflow-hidden text-sm leading-5 font-medium text-color whitespace-nowrap overflow-ellipsis">
|
||||||
{{ client.displayName || client.username }}
|
{{ client.displayName || client.username }}
|
||||||
</p>
|
|
||||||
|
|
||||||
<Component :is="expanded ? ChevronUp : ChevronDown" v-if="!isMe" :size="20" class="text-muted-color" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="hasBadges" class="flex justify-end align-center gap-1 mt-2">
|
<div class="flex align-center gap-1">
|
||||||
<PrimeBadge v-if="streaming" v-tooltip.top="'Watch'" severity="success" value="Streaming" size="small" @click.stop="watchStream" />
|
<PrimeBadge v-if="!!shareConsumer" v-tooltip.top="'Watch'" severity="success" value="Streaming" size="small" @click.stop="watchStream" />
|
||||||
|
|
||||||
<PrimeBadge v-if="premuted" severity="danger" value="Muted" size="small" />
|
<PrimeBadge v-if="premuted" severity="danger" value="Muted" size="small" />
|
||||||
<PrimeBadge v-else-if="client.outputMuted" severity="info" value="No sound" size="small" />
|
<PrimeBadge v-else-if="client.outputMuted" severity="info" value="No sound" size="small" />
|
||||||
@@ -36,6 +26,8 @@
|
|||||||
|
|
||||||
<PrimeBadge v-if="isMe" severity="secondary" value="You" size="small" class="shrink-0" />
|
<PrimeBadge v-if="isMe" severity="secondary" value="You" size="small" class="shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Component :is="expanded ? ChevronUp : ChevronDown" v-if="!isMe" :size="20" class="text-muted-color" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CollapseTransition v-if="!isMe">
|
<CollapseTransition v-if="!isMe">
|
||||||
@@ -60,6 +52,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ChadClient } from '#shared/types'
|
import type { ChadClient } from '#shared/types'
|
||||||
|
import { useLocalStorage } from '@vueuse/core'
|
||||||
import { ChevronDown, ChevronUp, User } from 'lucide-vue-next'
|
import { ChevronDown, ChevronUp, User } from 'lucide-vue-next'
|
||||||
import CollapseTransition from '~/components/CollapseTransition.vue'
|
import CollapseTransition from '~/components/CollapseTransition.vue'
|
||||||
|
|
||||||
@@ -74,34 +67,34 @@ const { show } = useFullscreenVideo()
|
|||||||
|
|
||||||
const expanded = ref(false)
|
const expanded = ref(false)
|
||||||
|
|
||||||
const {
|
const volume = useLocalStorage<number>(computed(() => `CLIENT_VOLUME_${props.client.userId}`), 100, { writeDefaults: false })
|
||||||
volume,
|
const premuted = useLocalStorage<boolean>(computed(() => `CLIENT_PREMUTED_${props.client.userId}`), false, { writeDefaults: false })
|
||||||
premuted,
|
|
||||||
speaking,
|
|
||||||
audioConsumers,
|
|
||||||
videoConsumers,
|
|
||||||
shareConsumers,
|
|
||||||
streaming,
|
|
||||||
} = useClient(toRef(() => props.client.socketId))
|
|
||||||
|
|
||||||
const isMe = computed(() => {
|
const isMe = computed(() => {
|
||||||
return me.value && props.client.userId === me.value.userId
|
return me.value && props.client.userId === me.value.userId
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const consumers = computed(() => {
|
||||||
|
return allConsumers.value.values().filter(consumer => consumer.appData.socketId === props.client.socketId).toArray()
|
||||||
|
})
|
||||||
|
|
||||||
const audioConsumer = computed(() => {
|
const audioConsumer = computed(() => {
|
||||||
return audioConsumers.value[0]
|
return consumers.value.find(consumer => consumer.track.kind === 'audio')
|
||||||
|
})
|
||||||
|
|
||||||
|
const videoConsumers = computed(() => {
|
||||||
|
return consumers.value.filter(consumer => consumer.track.kind === 'video')
|
||||||
|
})
|
||||||
|
|
||||||
|
const shareConsumer = computed(() => {
|
||||||
|
return videoConsumers.value.find(consumer => consumer.appData.source === 'share')
|
||||||
})
|
})
|
||||||
|
|
||||||
const audioTrack = computed(() => {
|
const audioTrack = computed(() => {
|
||||||
return audioConsumer.value?.raw.track
|
return audioConsumer.value?.track
|
||||||
})
|
})
|
||||||
|
|
||||||
const audioConsumerPaused = computed(() => {
|
const audioConsumerPaused = ref(false)
|
||||||
if (Object.keys(allConsumers.value).length === 0)
|
|
||||||
return false
|
|
||||||
|
|
||||||
return audioConsumer.value?.paused ?? false
|
|
||||||
})
|
|
||||||
|
|
||||||
const inputMuted = computed(() => {
|
const inputMuted = computed(() => {
|
||||||
if (isMe.value)
|
if (isMe.value)
|
||||||
@@ -110,12 +103,8 @@ const inputMuted = computed(() => {
|
|||||||
return premuted.value || audioConsumerPaused.value
|
return premuted.value || audioConsumerPaused.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasBadges = computed(() => {
|
watch(allConsumers, () => {
|
||||||
return streaming.value
|
audioConsumerPaused.value = audioConsumer.value?.paused ?? false
|
||||||
|| premuted.value
|
|
||||||
|| inputMuted.value
|
|
||||||
|| props.client.outputMuted
|
|
||||||
|| isMe.value
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const { setGain } = useAudioContext(audioTrack)
|
const { setGain } = useAudioContext(audioTrack)
|
||||||
@@ -132,11 +121,9 @@ function toggleExpand() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function watchStream() {
|
function watchStream() {
|
||||||
if (!streaming.value)
|
if (!shareConsumer.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
const consumer = [...videoConsumers.value, ...shareConsumers.value][0]!
|
show(new MediaStream([shareConsumer.value.track]))
|
||||||
|
|
||||||
show(new MediaStream([consumer.raw.track]))
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="text-sm overflow-x-auto">
|
|
||||||
<p class="text-muted-color">
|
|
||||||
{{ consumer.id }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>paused: {{ consumer.paused }}</p>
|
|
||||||
|
|
||||||
<p v-for="[key, value] in appData" :key="key">
|
|
||||||
{{ key }}: {{ value }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Consumer } from 'mediasoup-client/types'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
consumer: Consumer
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const appData = computed(() => {
|
|
||||||
return Object.entries(props.consumer.appData)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Teleport to="body">
|
|
||||||
<div ref="root" class="fullscreen-gallery">
|
|
||||||
{{ videoConsumers.length + shareConsumers.length }}
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useFullscreen } from '@vueuse/core'
|
|
||||||
|
|
||||||
const rootRef = useTemplateRef('root')
|
|
||||||
|
|
||||||
const { enter } = useFullscreen(rootRef)
|
|
||||||
const { videoConsumers, shareConsumers } = useMediasoup()
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// enter()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="fullscreen-gallery-card">
|
|
||||||
sasd
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="group cursor-pointer hover:outline outline-primary relative rounded overflow-hidden flex items-center justify-center"
|
|
||||||
@click="watch"
|
|
||||||
>
|
|
||||||
<video :srcObject="stream" muted autoplay />
|
|
||||||
|
|
||||||
<PrimeTag
|
|
||||||
severity="secondary"
|
|
||||||
class="absolute bottom-2 text-sm opacity-70 group-hover:opacity-100"
|
|
||||||
rounded
|
|
||||||
>
|
|
||||||
{{ isMe ? 'You' : client.displayName }}
|
|
||||||
</PrimeTag>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { ChadClient } from '#shared/types'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
client: ChadClient
|
|
||||||
stream: MediaStream
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const { me } = useClients()
|
|
||||||
const fullscreenVideo = useFullscreenVideo()
|
|
||||||
|
|
||||||
const isMe = computed(() => {
|
|
||||||
return props.client.socketId === me.value?.socketId
|
|
||||||
})
|
|
||||||
|
|
||||||
function watch() {
|
|
||||||
fullscreenVideo.show(props.stream)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -8,7 +8,6 @@ export const useApp = createGlobalState(() => {
|
|||||||
const mediasoup = useMediasoup()
|
const mediasoup = useMediasoup()
|
||||||
const signaling = useSignaling()
|
const signaling = useSignaling()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const sfx = useSfx()
|
|
||||||
|
|
||||||
const ready = ref(false)
|
const ready = ref(false)
|
||||||
const isTauri = computed(() => '__TAURI_INTERNALS__' in window)
|
const isTauri = computed(() => '__TAURI_INTERNALS__' in window)
|
||||||
@@ -32,42 +31,29 @@ export const useApp = createGlobalState(() => {
|
|||||||
|
|
||||||
const outputMuted = ref(false)
|
const outputMuted = ref(false)
|
||||||
|
|
||||||
const videoEnabled = computed(() => {
|
|
||||||
return !!mediasoup.videoProducer.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const sharingEnabled = computed(() => {
|
const sharingEnabled = computed(() => {
|
||||||
return !!mediasoup.shareProducer.value
|
return !!mediasoup.shareProducer.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const somebodyStreamingVideo = computed(() => {
|
|
||||||
return !!mediasoup.videoProducer.value
|
|
||||||
|| !!mediasoup.shareProducer.value
|
|
||||||
|| mediasoup.videoConsumers.value.length > 0
|
|
||||||
|| mediasoup.shareConsumers.value.length > 0
|
|
||||||
})
|
|
||||||
|
|
||||||
async function muteInput() {
|
async function muteInput() {
|
||||||
if (inputMuted.value || !mediasoup.micProducer.value)
|
if (inputMuted.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
await mediasoup.pauseProducer(mediasoup.micProducer.value)
|
await mediasoup.pauseProducer('microphone')
|
||||||
|
|
||||||
sfx.play('/sfx/off_micr.ogg').then()
|
|
||||||
toast.add({ severity: 'info', summary: 'Microphone muted', closable: false, life: 1000 })
|
toast.add({ severity: 'info', summary: 'Microphone muted', closable: false, life: 1000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unmuteInput() {
|
async function unmuteInput() {
|
||||||
if (!inputMuted.value || !mediasoup.micProducer.value)
|
if (!inputMuted.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
if (outputMuted.value) {
|
if (outputMuted.value) {
|
||||||
await unmuteOutput()
|
await unmuteOutput()
|
||||||
}
|
}
|
||||||
|
|
||||||
await mediasoup.resumeProducer(mediasoup.micProducer.value)
|
await mediasoup.resumeProducer('microphone')
|
||||||
|
|
||||||
sfx.play('/sfx/on_micr.ogg').then()
|
|
||||||
toast.add({ severity: 'info', summary: 'Microphone activated', closable: false, life: 1000 })
|
toast.add({ severity: 'info', summary: 'Microphone activated', closable: false, life: 1000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,25 +101,12 @@ export const useApp = createGlobalState(() => {
|
|||||||
await muteOutput()
|
await muteOutput()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleVideo() {
|
|
||||||
if (!mediasoup.videoProducer.value) {
|
|
||||||
await mediasoup.enableVideo()
|
|
||||||
await sfx.play('/sfx/on_trans.ogg', 0.03)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
await mediasoup.disableProducer(mediasoup.videoProducer.value)
|
|
||||||
await sfx.play('/sfx/off_trans.ogg', 0.03)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleShare() {
|
async function toggleShare() {
|
||||||
if (!mediasoup.shareProducer.value) {
|
if (!mediasoup.shareProducer.value) {
|
||||||
await mediasoup.enableShare()
|
await mediasoup.enableShare()
|
||||||
await sfx.play('/sfx/on_trans.ogg', 0.03)
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await mediasoup.disableProducer(mediasoup.shareProducer.value)
|
await mediasoup.disableProducer('share')
|
||||||
await sfx.play('/sfx/off_trans.ogg', 0.03)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,13 +121,10 @@ export const useApp = createGlobalState(() => {
|
|||||||
muteOutput,
|
muteOutput,
|
||||||
unmuteOutput,
|
unmuteOutput,
|
||||||
toggleOutput,
|
toggleOutput,
|
||||||
toggleVideo,
|
|
||||||
version,
|
version,
|
||||||
isTauri,
|
isTauri,
|
||||||
commitSha,
|
commitSha,
|
||||||
toggleShare,
|
toggleShare,
|
||||||
videoEnabled,
|
|
||||||
sharingEnabled,
|
sharingEnabled,
|
||||||
somebodyStreamingVideo,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import type { ChadClient } from '#shared/types'
|
|
||||||
import { useLocalStorage } from '@vueuse/core'
|
|
||||||
|
|
||||||
export function useClient(socketId: MaybeRef<ChadClient['socketId']>) {
|
|
||||||
const mediasoup = useMediasoup()
|
|
||||||
const { getClient } = useClients()
|
|
||||||
|
|
||||||
const client = computed(() => getClient(unref(socketId))!)
|
|
||||||
|
|
||||||
const volume = useLocalStorage<number>(computed(() => `CLIENT_VOLUME_${client.value.userId}`), 100, { writeDefaults: false })
|
|
||||||
const premuted = useLocalStorage<boolean>(computed(() => `CLIENT_PREMUTED_${client.value.userId}`), false, { writeDefaults: false })
|
|
||||||
|
|
||||||
const consumers = computed(() => {
|
|
||||||
return Object.values(mediasoup.consumers.value).filter(consumer => consumer.appData.socketId === client.value.socketId)
|
|
||||||
})
|
|
||||||
|
|
||||||
const audioConsumers = computed(() => {
|
|
||||||
return mediasoup.audioConsumers.value.filter(consumer => consumer.appData.socketId === client.value.socketId)
|
|
||||||
})
|
|
||||||
|
|
||||||
const videoConsumers = computed(() => {
|
|
||||||
return mediasoup.videoConsumers.value.filter(consumer => consumer.appData.socketId === client.value.socketId)
|
|
||||||
})
|
|
||||||
|
|
||||||
const shareConsumers = computed(() => {
|
|
||||||
return mediasoup.shareConsumers.value.filter(consumer => consumer.appData.socketId === client.value.socketId)
|
|
||||||
})
|
|
||||||
|
|
||||||
const producers = computed(() => {
|
|
||||||
return Object.values(mediasoup.producers.value).filter(producer => producer.appData.socketId === client.value.socketId)
|
|
||||||
})
|
|
||||||
|
|
||||||
const streaming = computed(() => {
|
|
||||||
return videoConsumers.value.length > 0 || shareConsumers.value.length > 0
|
|
||||||
})
|
|
||||||
|
|
||||||
const speaking = computed(() => {
|
|
||||||
return mediasoup.speakingClients.value.some(speaker => speaker.clientId === client.value.socketId)
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
volume,
|
|
||||||
premuted,
|
|
||||||
consumers,
|
|
||||||
producers,
|
|
||||||
audioConsumers,
|
|
||||||
videoConsumers,
|
|
||||||
shareConsumers,
|
|
||||||
streaming,
|
|
||||||
speaking,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,16 +19,7 @@ export const useDevices = createGlobalState(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
;(async () => {
|
|
||||||
if (permissionGranted.value)
|
|
||||||
return
|
|
||||||
|
|
||||||
await ensurePermissions()
|
|
||||||
})()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ensurePermissions,
|
|
||||||
permissionGranted,
|
|
||||||
videoInputs: computed<MediaDeviceInfo[]>(() => JSON.parse(JSON.stringify(videoInputs.value))),
|
videoInputs: computed<MediaDeviceInfo[]>(() => JSON.parse(JSON.stringify(videoInputs.value))),
|
||||||
audioInputs: computed<MediaDeviceInfo[]>(() => JSON.parse(JSON.stringify(audioInputs.value))),
|
audioInputs: computed<MediaDeviceInfo[]>(() => JSON.parse(JSON.stringify(audioInputs.value))),
|
||||||
audioOutputs: computed<MediaDeviceInfo[]>(() => JSON.parse(JSON.stringify(audioOutputs.value))),
|
audioOutputs: computed<MediaDeviceInfo[]>(() => JSON.parse(JSON.stringify(audioOutputs.value))),
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { createSharedComposable } from '@vueuse/core'
|
|
||||||
|
|
||||||
export const useFullscreenGallery = createSharedComposable(() => {
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
@@ -1,18 +1,11 @@
|
|||||||
import type { ChadClient, Consumer, Producer } from '#shared/types'
|
|
||||||
import type { MediaKind, ProducerOptions } from 'mediasoup-client/types'
|
import type { MediaKind, ProducerOptions } from 'mediasoup-client/types'
|
||||||
import { createSharedComposable } from '@vueuse/core'
|
import { createSharedComposable } from '@vueuse/core'
|
||||||
import * as mediasoupClient from 'mediasoup-client'
|
import * as mediasoupClient from 'mediasoup-client'
|
||||||
import { shallowRef } from 'vue'
|
|
||||||
import { useDevices } from '~/composables/use-devices'
|
import { useDevices } from '~/composables/use-devices'
|
||||||
import { usePreferences } from '~/composables/use-preferences'
|
import { usePreferences } from '~/composables/use-preferences'
|
||||||
import { useSignaling } from '~/composables/use-signaling'
|
import { useSignaling } from '~/composables/use-signaling'
|
||||||
|
|
||||||
type ProducerType = 'microphone' | 'video' | 'share'
|
type ProducerType = 'microphone' | 'camera' | 'share'
|
||||||
|
|
||||||
interface SpeakingClient {
|
|
||||||
clientId: ChadClient['socketId']
|
|
||||||
volume: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const ICE_SERVERS: RTCIceServer[] = [
|
const ICE_SERVERS: RTCIceServer[] = [
|
||||||
{ urls: 'stun:stun.l.google.com:19302' },
|
{ urls: 'stun:stun.l.google.com:19302' },
|
||||||
@@ -29,7 +22,6 @@ const ICE_SERVERS: RTCIceServer[] = [
|
|||||||
|
|
||||||
export const useMediasoup = createSharedComposable(() => {
|
export const useMediasoup = createSharedComposable(() => {
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const sfx = useSfx()
|
|
||||||
|
|
||||||
const signaling = useSignaling()
|
const signaling = useSignaling()
|
||||||
const { addClient, removeClient } = useClients()
|
const { addClient, removeClient } = useClients()
|
||||||
@@ -41,42 +33,12 @@ export const useMediasoup = createSharedComposable(() => {
|
|||||||
const sendTransport = shallowRef<mediasoupClient.types.Transport>()
|
const sendTransport = shallowRef<mediasoupClient.types.Transport>()
|
||||||
const recvTransport = shallowRef<mediasoupClient.types.Transport>()
|
const recvTransport = shallowRef<mediasoupClient.types.Transport>()
|
||||||
|
|
||||||
const consumers = ref<Record<Consumer['id'], Consumer>>({})
|
const micProducer = shallowRef<mediasoupClient.types.Producer>()
|
||||||
const producers = ref<Record<Producer['id'], Producer>>({})
|
const cameraProducer = shallowRef<mediasoupClient.types.Producer>()
|
||||||
|
const shareProducer = shallowRef<mediasoupClient.types.Producer>()
|
||||||
|
|
||||||
const consumersArray = computed(() => {
|
const consumers = shallowRef<Map<string, mediasoupClient.types.Consumer>>(new Map())
|
||||||
return Object.values(consumers.value)
|
const producers = shallowRef<Map<string, mediasoupClient.types.Producer>>(new Map())
|
||||||
})
|
|
||||||
|
|
||||||
const audioConsumers = computed(() => {
|
|
||||||
return consumersArray.value.filter(consumer => consumer.raw.kind === 'audio')
|
|
||||||
})
|
|
||||||
|
|
||||||
const videoConsumers = computed(() => {
|
|
||||||
return consumersArray.value.filter(consumer => consumer.raw.kind === 'video' && consumer.appData.source !== 'share')
|
|
||||||
})
|
|
||||||
|
|
||||||
const shareConsumers = computed(() => {
|
|
||||||
return consumersArray.value.filter(consumer => consumer.raw.kind === 'video' && consumer.appData.source === 'share')
|
|
||||||
})
|
|
||||||
|
|
||||||
const producersArray = computed(() => {
|
|
||||||
return Object.values(producers.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const micProducer = computed(() => {
|
|
||||||
return producersArray.value.find(producer => producer.raw.kind === 'audio' && producer.raw.appData.source === 'mic-video')
|
|
||||||
})
|
|
||||||
|
|
||||||
const videoProducer = computed(() => {
|
|
||||||
return producersArray.value.find(producer => producer.raw.kind === 'video' && producer.raw.appData.source !== 'share')
|
|
||||||
})
|
|
||||||
|
|
||||||
const shareProducer = computed(() => {
|
|
||||||
return producersArray.value.find(producer => producer.raw.kind === 'video' && producer.raw.appData.source === 'share')
|
|
||||||
})
|
|
||||||
|
|
||||||
const speakingClients = shallowRef<SpeakingClient[]>([])
|
|
||||||
|
|
||||||
watch(signaling.socket, (socket) => {
|
watch(signaling.socket, (socket) => {
|
||||||
if (!socket)
|
if (!socket)
|
||||||
@@ -176,7 +138,6 @@ export const useMediasoup = createSharedComposable(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
socket.on('newPeer', (client) => {
|
socket.on('newPeer', (client) => {
|
||||||
sfx.playRandomConnectionSound(client.socketId).then()
|
|
||||||
addClient(client)
|
addClient(client)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -198,38 +159,20 @@ export const useMediasoup = createSharedComposable(() => {
|
|||||||
producerId,
|
producerId,
|
||||||
kind,
|
kind,
|
||||||
rtpParameters,
|
rtpParameters,
|
||||||
streamId: `${socketId}-${appData.source || 'stream'}`,
|
streamId: `${socketId}-${appData.source === 'share' ? 'share' : 'mic-webcam'}`,
|
||||||
appData: { ...appData, socketId },
|
appData: { ...appData, socketId },
|
||||||
})
|
})
|
||||||
|
|
||||||
if (kind === 'video')
|
|
||||||
sfx.play('/sfx/on_trans.ogg', 0.03).then()
|
|
||||||
|
|
||||||
if (producerPaused)
|
if (producerPaused)
|
||||||
consumer.pause()
|
consumer.pause()
|
||||||
|
|
||||||
consumers.value[consumer.id] = {
|
consumer.on('transportclose', () => {
|
||||||
id: consumer.id,
|
if (consumers.value.delete(consumer.id))
|
||||||
paused: consumer.paused,
|
triggerRef(consumers)
|
||||||
appData: consumer.appData,
|
|
||||||
raw: markRaw(consumer),
|
|
||||||
}
|
|
||||||
|
|
||||||
consumer.observer.on('resume', () => {
|
|
||||||
consumers.value[consumer.id]!.paused = false
|
|
||||||
})
|
})
|
||||||
|
|
||||||
consumer.observer.on('pause', () => {
|
consumers.value.set(consumer.id, consumer)
|
||||||
consumers.value[consumer.id]!.paused = true
|
triggerRef(consumers)
|
||||||
})
|
|
||||||
|
|
||||||
consumer.observer.on('close', () => {
|
|
||||||
delete consumers.value[consumer.id]
|
|
||||||
})
|
|
||||||
|
|
||||||
consumer.on('trackended', () => {
|
|
||||||
consumer.close()
|
|
||||||
})
|
|
||||||
|
|
||||||
cb()
|
cb()
|
||||||
},
|
},
|
||||||
@@ -240,37 +183,11 @@ export const useMediasoup = createSharedComposable(() => {
|
|||||||
async (
|
async (
|
||||||
{ consumerId },
|
{ consumerId },
|
||||||
) => {
|
) => {
|
||||||
const consumer = consumers.value[consumerId]
|
if (consumers.value.delete(consumerId))
|
||||||
|
triggerRef(consumers)
|
||||||
if (!consumer)
|
|
||||||
return
|
|
||||||
|
|
||||||
consumer.raw.close()
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
socket.on('consumerPaused', ({ consumerId }) => {
|
|
||||||
const consumer = consumers.value[consumerId]
|
|
||||||
|
|
||||||
if (!consumer)
|
|
||||||
return
|
|
||||||
|
|
||||||
consumer.raw.pause()
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on('consumerResumed', ({ consumerId }) => {
|
|
||||||
const consumer = consumers.value[consumerId]
|
|
||||||
|
|
||||||
if (!consumer)
|
|
||||||
return
|
|
||||||
|
|
||||||
consumer.raw.resume()
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on('speakingPeers', (value: SpeakingClient[]) => {
|
|
||||||
speakingClients.value = value
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
device.value = undefined
|
device.value = undefined
|
||||||
rtpCapabilities.value = undefined
|
rtpCapabilities.value = undefined
|
||||||
@@ -281,12 +198,43 @@ export const useMediasoup = createSharedComposable(() => {
|
|||||||
recvTransport.value?.close()
|
recvTransport.value?.close()
|
||||||
recvTransport.value = undefined
|
recvTransport.value = undefined
|
||||||
|
|
||||||
consumers.value = {}
|
micProducer.value = undefined
|
||||||
producers.value = {}
|
cameraProducer.value = undefined
|
||||||
|
shareProducer.value = undefined
|
||||||
|
|
||||||
|
consumers.value = new Map()
|
||||||
|
producers.value = new Map()
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('consumerPaused', ({ consumerId }) => {
|
||||||
|
const consumer = consumers.value.get(consumerId)
|
||||||
|
|
||||||
|
if (!consumer)
|
||||||
|
return
|
||||||
|
|
||||||
|
consumer.pause()
|
||||||
|
|
||||||
|
triggerRef(consumers)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('consumerResumed', ({ consumerId }) => {
|
||||||
|
const consumer = consumers.value.get(consumerId)
|
||||||
|
|
||||||
|
if (!consumer)
|
||||||
|
return
|
||||||
|
|
||||||
|
consumer.resume()
|
||||||
|
|
||||||
|
triggerRef(consumers)
|
||||||
})
|
})
|
||||||
}, { immediate: true, flush: 'sync' })
|
}, { immediate: true, flush: 'sync' })
|
||||||
|
|
||||||
async function createProducer(options: ProducerOptions) {
|
async function enableProducer(type: ProducerType, options: ProducerOptions) {
|
||||||
|
const producer = getProducerByType(type)
|
||||||
|
|
||||||
|
if (producer.value)
|
||||||
|
return
|
||||||
|
|
||||||
if (!device.value || !sendTransport.value)
|
if (!device.value || !sendTransport.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -296,54 +244,47 @@ export const useMediasoup = createSharedComposable(() => {
|
|||||||
if (!device.value.canProduce(options.track.kind as MediaKind))
|
if (!device.value.canProduce(options.track.kind as MediaKind))
|
||||||
return
|
return
|
||||||
|
|
||||||
const producer = await sendTransport.value.produce({ disableTrackOnPause: true, ...options })
|
producer.value = await sendTransport.value.produce(options)
|
||||||
|
|
||||||
producers.value[producer.id] = {
|
producers.value.set(producer.value.id, producer.value)
|
||||||
id: producer.id,
|
triggerRef(producers)
|
||||||
paused: producer.paused,
|
triggerRef(producer)
|
||||||
appData: producer.appData,
|
|
||||||
raw: markRaw(producer),
|
|
||||||
}
|
|
||||||
|
|
||||||
producer.observer.on('pause', () => {
|
producer.value.on('transportclose', () => {
|
||||||
producers.value[producer.id]!.paused = true
|
micProducer.value = undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
producer.observer.on('resume', () => {
|
producer.value.on('trackended', () => {
|
||||||
producers.value[producer.id]!.paused = false
|
disableProducer(type)
|
||||||
})
|
|
||||||
|
|
||||||
producer.observer.on('close', () => {
|
|
||||||
delete producers.value[producer.id]
|
|
||||||
})
|
|
||||||
|
|
||||||
producer.on('trackended', () => {
|
|
||||||
disableProducer(producers.value[producer.id]!)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function disableProducer(producer: Producer) {
|
async function disableProducer(type: ProducerType) {
|
||||||
if (!signaling.socket.value)
|
const producer = getProducerByType(type)
|
||||||
|
|
||||||
|
if (!signaling.socket.value || !producer.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
producers.value.delete(producer.value.id)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
producer.raw.close()
|
producer.value.close()
|
||||||
|
|
||||||
await signaling.socket.value.emitWithAck('closeProducer', {
|
await signaling.socket.value.emitWithAck('closeProducer', {
|
||||||
producerId: producer.id,
|
producerId: producer.value.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
delete producers.value[producer.id]
|
triggerRef(producers)
|
||||||
|
triggerRef(producer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
producer.value = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enableMic() {
|
async function enableMic() {
|
||||||
if (micProducer.value)
|
|
||||||
return
|
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: {
|
audio: {
|
||||||
deviceId: { exact: preferences.inputDeviceId.value },
|
deviceId: { exact: preferences.inputDeviceId.value },
|
||||||
@@ -358,67 +299,21 @@ export const useMediasoup = createSharedComposable(() => {
|
|||||||
if (!track)
|
if (!track)
|
||||||
return
|
return
|
||||||
|
|
||||||
await createProducer({
|
await enableProducer('microphone', {
|
||||||
track,
|
track,
|
||||||
streamId: 'mic-video',
|
|
||||||
codecOptions: {
|
codecOptions: {
|
||||||
opusStereo: true,
|
opusStereo: true,
|
||||||
opusDtx: true, // Меньше пакетов летит когда тишина
|
opusDtx: true, // Меньше пакетов летит когда тишина
|
||||||
opusFec: false, // Фиксит пакет лос
|
opusFec: false, // Фиксит пакет лос
|
||||||
},
|
},
|
||||||
appData: {
|
|
||||||
source: 'mic-video',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function disableMic() {
|
async function disableMic() {
|
||||||
if (!micProducer.value)
|
await disableProducer('microphone')
|
||||||
return
|
|
||||||
|
|
||||||
await disableProducer(micProducer.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function enableVideo() {
|
|
||||||
if (videoProducer.value)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (!device.value)
|
|
||||||
return
|
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
video: {
|
|
||||||
deviceId: { exact: preferences.videoDeviceId.value },
|
|
||||||
width: { ideal: 1920 },
|
|
||||||
height: { ideal: 1080 },
|
|
||||||
frameRate: { ideal: 60 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const track = stream.getVideoTracks()[0]
|
|
||||||
|
|
||||||
if (!track)
|
|
||||||
return
|
|
||||||
|
|
||||||
await createProducer({
|
|
||||||
track,
|
|
||||||
streamId: 'mic-video',
|
|
||||||
// codec: device.value.rtpCapabilities.codecs?.find(
|
|
||||||
// c => c.mimeType.toLowerCase() === 'video/AV1',
|
|
||||||
// ),
|
|
||||||
// codecOptions: {
|
|
||||||
// videoGoogleStartBitrate: 1000,
|
|
||||||
// },
|
|
||||||
appData: {
|
|
||||||
source: 'mic-video',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enableShare() {
|
async function enableShare() {
|
||||||
if (shareProducer.value)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (!device.value)
|
if (!device.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -429,54 +324,85 @@ export const useMediasoup = createSharedComposable(() => {
|
|||||||
if (!track)
|
if (!track)
|
||||||
return
|
return
|
||||||
|
|
||||||
await createProducer({
|
await enableProducer('share', {
|
||||||
track,
|
track,
|
||||||
streamId: 'share',
|
|
||||||
codec: device.value.rtpCapabilities.codecs?.find(
|
codec: device.value.rtpCapabilities.codecs?.find(
|
||||||
c => c.mimeType.toLowerCase() === 'video/AV1',
|
c => c.mimeType.toLowerCase() === 'video/h264',
|
||||||
),
|
),
|
||||||
codecOptions: {
|
codecOptions: {
|
||||||
videoGoogleStartBitrate: 1000,
|
videoGoogleStartBitrate: 1000,
|
||||||
},
|
},
|
||||||
zeroRtpOnPause: true,
|
|
||||||
appData: {
|
appData: {
|
||||||
source: 'share',
|
source: 'share',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pauseProducer(producer: Producer) {
|
async function pauseProducer(type: ProducerType) {
|
||||||
if (!signaling.socket.value)
|
if (!signaling.socket.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
if (producer.paused)
|
const producer = getProducerByType(type)
|
||||||
|
|
||||||
|
if (!producer.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (producer.value.paused)
|
||||||
return
|
return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
producer.raw.pause()
|
producer.value.pause()
|
||||||
|
|
||||||
await signaling.socket.value.emitWithAck('pauseProducer', {
|
await signaling.socket.value.emitWithAck('pauseProducer', {
|
||||||
producerId: producer.id,
|
producerId: producer.value.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
producer.raw.resume()
|
producer.value.resume()
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
triggerRef(producers)
|
||||||
|
triggerRef(producer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resumeProducer(producer: Producer) {
|
async function resumeProducer(type: ProducerType) {
|
||||||
if (!signaling.socket.value)
|
if (!signaling.socket.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
const producer = getProducerByType(type)
|
||||||
|
|
||||||
|
if (!producer.value)
|
||||||
|
return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
producer.raw.resume()
|
producer.value.resume()
|
||||||
|
|
||||||
await signaling.socket.value.emitWithAck('resumeProducer', {
|
await signaling.socket.value.emitWithAck('resumeProducer', {
|
||||||
producerId: producer.id,
|
producerId: producer.value.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
producer.raw.pause()
|
producer.value.pause()
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
triggerRef(producers)
|
||||||
|
triggerRef(producer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
signaling.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProducerByType(type: ProducerType) {
|
||||||
|
switch (type) {
|
||||||
|
case 'microphone':
|
||||||
|
return micProducer
|
||||||
|
case 'camera':
|
||||||
|
return cameraProducer
|
||||||
|
case 'share':
|
||||||
|
return shareProducer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,22 +421,18 @@ export const useMediasoup = createSharedComposable(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
init,
|
||||||
consumers,
|
consumers,
|
||||||
audioConsumers,
|
|
||||||
videoConsumers,
|
|
||||||
shareConsumers,
|
|
||||||
producers,
|
producers,
|
||||||
speakingClients,
|
|
||||||
sendTransport,
|
sendTransport,
|
||||||
recvTransport,
|
recvTransport,
|
||||||
rtpCapabilities,
|
rtpCapabilities,
|
||||||
device,
|
device,
|
||||||
micProducer,
|
micProducer,
|
||||||
videoProducer,
|
cameraProducer,
|
||||||
shareProducer,
|
shareProducer,
|
||||||
pauseProducer,
|
pauseProducer,
|
||||||
resumeProducer,
|
resumeProducer,
|
||||||
enableVideo,
|
|
||||||
enableShare,
|
enableShare,
|
||||||
disableProducer,
|
disableProducer,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,10 @@ export interface SyncedPreferences {
|
|||||||
export const usePreferences = createGlobalState(() => {
|
export const usePreferences = createGlobalState(() => {
|
||||||
const { videoInputs, audioInputs, audioOutputs } = useDevices()
|
const { videoInputs, audioInputs, audioOutputs } = useDevices()
|
||||||
|
|
||||||
const synced = ref(false)
|
const fetched = ref(false)
|
||||||
|
|
||||||
const inputDeviceId = useLocalStorage<MediaDeviceInfo['deviceId']>('INPUT_DEVICE_ID', 'default')
|
const inputDeviceId = useLocalStorage<MediaDeviceInfo['deviceId']>('INPUT_DEVICE_ID', 'default')
|
||||||
const outputDeviceId = useLocalStorage<MediaDeviceInfo['deviceId']>('OUTPUT_DEVICE_ID', 'default')
|
const outputDeviceId = useLocalStorage<MediaDeviceInfo['deviceId']>('OUTPUT_DEVICE_ID', 'default')
|
||||||
const videoDeviceId = useLocalStorage<MediaDeviceInfo['deviceId']>('VIDEO_DEVICE_ID', 'default')
|
|
||||||
|
|
||||||
const autoGainControl = useLocalStorage('AUTO_GAIN_CONTROL', false)
|
const autoGainControl = useLocalStorage('AUTO_GAIN_CONTROL', false)
|
||||||
const noiseSuppression = useLocalStorage('NOISE_SUPPRESSION', true)
|
const noiseSuppression = useLocalStorage('NOISE_SUPPRESSION', true)
|
||||||
@@ -33,10 +32,6 @@ export const usePreferences = createGlobalState(() => {
|
|||||||
return audioOutputs.value.some(device => device.deviceId === outputDeviceId.value)
|
return audioOutputs.value.some(device => device.deviceId === outputDeviceId.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const videoDeviceExist = computed(() => {
|
|
||||||
return videoInputs.value.some(device => device.deviceId === videoDeviceId.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
watchDebounced(
|
watchDebounced(
|
||||||
[toggleInputHotkey, toggleOutputHotkey],
|
[toggleInputHotkey, toggleOutputHotkey],
|
||||||
async ([toggleInputHotkey, toggleOutputHotkey]) => {
|
async ([toggleInputHotkey, toggleOutputHotkey]) => {
|
||||||
@@ -58,10 +53,9 @@ export const usePreferences = createGlobalState(() => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
synced,
|
fetched,
|
||||||
inputDeviceId,
|
inputDeviceId,
|
||||||
outputDeviceId,
|
outputDeviceId,
|
||||||
videoDeviceId,
|
|
||||||
autoGainControl,
|
autoGainControl,
|
||||||
noiseSuppression,
|
noiseSuppression,
|
||||||
echoCancellation,
|
echoCancellation,
|
||||||
@@ -70,6 +64,5 @@ export const usePreferences = createGlobalState(() => {
|
|||||||
toggleOutputHotkey,
|
toggleOutputHotkey,
|
||||||
inputDeviceExist,
|
inputDeviceExist,
|
||||||
outputDeviceExist,
|
outputDeviceExist,
|
||||||
videoDeviceExist,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import { createSharedComposable } from '@vueuse/core'
|
|
||||||
import { Howl, Howler } from 'howler'
|
|
||||||
|
|
||||||
const CONNECTION_SOUNDS = Object.keys(import.meta.glob('@/../public/sfx/connection/*.ogg')).map(path => path.replace('../public', ''))
|
|
||||||
|
|
||||||
console.log('CONNECTION_SOUNDS', CONNECTION_SOUNDS)
|
|
||||||
|
|
||||||
function hashStringToNumber(str: string, cap: number): number {
|
|
||||||
let hash = 0
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
hash = (hash * 31 + str.charCodeAt(i)) | 0
|
|
||||||
}
|
|
||||||
return Math.abs(hash) % cap
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSfx = createSharedComposable(() => {
|
|
||||||
async function play(src: string, volume = 0.2): Promise<void> {
|
|
||||||
Howler.stop()
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const howl = new Howl({
|
|
||||||
src,
|
|
||||||
autoplay: true,
|
|
||||||
loop: false,
|
|
||||||
volume,
|
|
||||||
})
|
|
||||||
|
|
||||||
howl.on('end', () => {
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function playRandomConnectionSound(seed: string) {
|
|
||||||
await play('/sfx/on_trans.ogg', 0.03)
|
|
||||||
await play(CONNECTION_SOUNDS[hashStringToNumber(seed, CONNECTION_SOUNDS.length + 1)]!, 0.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
play,
|
|
||||||
playRandomConnectionSound,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="grid grid-cols-[360px_1fr] gap-2 p-2 h-screen grid-rows-[auto_1fr] max-w-full">
|
<div class="grid grid-cols-[1fr_1.25fr] gap-2 p-2 h-screen grid-rows-[auto_1fr]">
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between gap-2 rounded-xl p-3 bg-surface-950"
|
class="flex items-center justify-between gap-2 rounded-xl p-3 bg-surface-950"
|
||||||
>
|
>
|
||||||
@@ -21,12 +21,6 @@
|
|||||||
</PrimeButton>
|
</PrimeButton>
|
||||||
</PrimeButtonGroup>
|
</PrimeButtonGroup>
|
||||||
|
|
||||||
<PrimeButton :severity="videoEnabled ? 'success' : undefined" outlined @click="toggleVideo">
|
|
||||||
<template #icon>
|
|
||||||
<Component :is="videoEnabled ? CameraOff : Camera" />
|
|
||||||
</template>
|
|
||||||
</PrimeButton>
|
|
||||||
|
|
||||||
<PrimeButton :severity="sharingEnabled ? 'success' : undefined" outlined @click="toggleShare">
|
<PrimeButton :severity="sharingEnabled ? 'success' : undefined" outlined @click="toggleShare">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Component :is="sharingEnabled ? ScreenShareOff : ScreenShare" />
|
<Component :is="sharingEnabled ? ScreenShareOff : ScreenShare" />
|
||||||
@@ -40,7 +34,7 @@
|
|||||||
<PrimeSelectButton
|
<PrimeSelectButton
|
||||||
v-model="activeTab"
|
v-model="activeTab"
|
||||||
:options="tabs"
|
:options="tabs"
|
||||||
option-label="id"
|
data-key="id"
|
||||||
:allow-empty="false"
|
:allow-empty="false"
|
||||||
style="--p-togglebutton-content-padding: 0.25rem 0.5rem"
|
style="--p-togglebutton-content-padding: 0.25rem 0.5rem"
|
||||||
>
|
>
|
||||||
@@ -50,33 +44,28 @@
|
|||||||
</PrimeSelectButton>
|
</PrimeSelectButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PrimeScrollPanel class="bg-surface-900 rounded-xl overflow-hidden" style="min-height: 0">
|
<PrimeScrollPanel class="bg-surface-900 rounded-xl" style="min-height: 0">
|
||||||
<div v-auto-animate class="p-3 space-y-1">
|
<div v-auto-animate class="p-3 space-y-1">
|
||||||
<ClientRow v-for="client of clients" :key="client.userId" :client="client" />
|
<ClientRow v-for="client of clients" :key="client.userId" :client="client" />
|
||||||
</div>
|
</div>
|
||||||
</PrimeScrollPanel>
|
</PrimeScrollPanel>
|
||||||
|
|
||||||
<PrimeScrollPanel class="bg-surface-900 rounded-xl overflow-hidden" style="min-height: 0">
|
<PrimeScrollPanel class="bg-surface-900 rounded-xl" style="min-height: 0">
|
||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</PrimeScrollPanel>
|
</PrimeScrollPanel>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FullscreenGallery />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
Camera,
|
|
||||||
CameraOff,
|
|
||||||
MessageCircle,
|
MessageCircle,
|
||||||
Mic,
|
Mic,
|
||||||
MicOff,
|
MicOff,
|
||||||
ScreenShare,
|
ScreenShare,
|
||||||
ScreenShareOff,
|
ScreenShareOff,
|
||||||
Settings,
|
Settings,
|
||||||
TvMinimalPlay,
|
|
||||||
UserPen,
|
UserPen,
|
||||||
Volume2,
|
Volume2,
|
||||||
VolumeOff,
|
VolumeOff,
|
||||||
@@ -87,12 +76,9 @@ const {
|
|||||||
clients,
|
clients,
|
||||||
inputMuted,
|
inputMuted,
|
||||||
outputMuted,
|
outputMuted,
|
||||||
videoEnabled,
|
|
||||||
sharingEnabled,
|
sharingEnabled,
|
||||||
somebodyStreamingVideo,
|
|
||||||
toggleInput,
|
toggleInput,
|
||||||
toggleOutput,
|
toggleOutput,
|
||||||
toggleVideo,
|
|
||||||
toggleShare,
|
toggleShare,
|
||||||
} = useApp()
|
} = useApp()
|
||||||
const { connect, connected } = useSignaling()
|
const { connect, connected } = useSignaling()
|
||||||
@@ -105,47 +91,31 @@ interface Tab {
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const tabs = computed<Tab[]>(() => {
|
const tabs: Tab[] = [
|
||||||
const result = []
|
{
|
||||||
|
id: 'Index',
|
||||||
if (somebodyStreamingVideo.value) {
|
icon: MessageCircle,
|
||||||
result.push({
|
onClick: () => {
|
||||||
id: 'Gallery',
|
navigateTo({ name: 'Index' })
|
||||||
icon: TvMinimalPlay,
|
|
||||||
onClick: () => {
|
|
||||||
navigateTo({ name: 'Gallery' })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(
|
|
||||||
{
|
|
||||||
id: 'Index',
|
|
||||||
icon: MessageCircle,
|
|
||||||
onClick: () => {
|
|
||||||
navigateTo({ name: 'Index' })
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
id: 'Profile',
|
{
|
||||||
icon: UserPen,
|
id: 'Profile',
|
||||||
onClick: () => {
|
icon: UserPen,
|
||||||
navigateTo({ name: 'Profile' })
|
onClick: () => {
|
||||||
},
|
navigateTo({ name: 'Profile' })
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
id: 'Preferences',
|
{
|
||||||
icon: Settings,
|
id: 'Preferences',
|
||||||
onClick: () => {
|
icon: Settings,
|
||||||
navigateTo({ name: 'Preferences' })
|
onClick: () => {
|
||||||
},
|
navigateTo({ name: 'Preferences' })
|
||||||
},
|
},
|
||||||
)
|
},
|
||||||
|
]
|
||||||
|
|
||||||
return result
|
const activeTab = ref<Tab>(tabs.find(tab => tab.id === route.name) ?? tabs[0]!)
|
||||||
})
|
|
||||||
|
|
||||||
const activeTab = ref<Tab>(tabs.value.find(tab => tab.id === route.name) ?? tabs.value.find(tab => tab.id === 'Index')!)
|
|
||||||
|
|
||||||
watch(activeTab, (activeTab) => {
|
watch(activeTab, (activeTab) => {
|
||||||
activeTab.onClick()
|
activeTab.onClick()
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ export default defineNuxtRouteMiddleware(async () => {
|
|||||||
if (!me.value)
|
if (!me.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
const { synced, toggleInputHotkey, toggleOutputHotkey } = usePreferences()
|
const { fetched, toggleInputHotkey, toggleOutputHotkey } = usePreferences()
|
||||||
|
|
||||||
if (synced.value)
|
if (fetched.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -20,7 +20,7 @@ export default defineNuxtRouteMiddleware(async () => {
|
|||||||
|
|
||||||
toggleInputHotkey.value = preferences.toggleInputHotkey ?? toggleInputHotkey.value
|
toggleInputHotkey.value = preferences.toggleInputHotkey ?? toggleInputHotkey.value
|
||||||
toggleOutputHotkey.value = preferences.toggleOutputHotkey ?? toggleOutputHotkey.value
|
toggleOutputHotkey.value = preferences.toggleOutputHotkey ?? toggleOutputHotkey.value
|
||||||
synced.value = true
|
fetched.value = true
|
||||||
}
|
}
|
||||||
catch {}
|
catch {}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="grid grid-cols-[1fr_1fr] gap-2">
|
|
||||||
<GalleryCard
|
|
||||||
v-for="item in gallery"
|
|
||||||
:key="item.client.socketId"
|
|
||||||
:client="item.client"
|
|
||||||
:stream="item.stream"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { ChadClient } from '#shared/types'
|
|
||||||
|
|
||||||
interface GalleryItem {
|
|
||||||
client: ChadClient
|
|
||||||
stream: MediaStream
|
|
||||||
}
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
name: 'Gallery',
|
|
||||||
})
|
|
||||||
|
|
||||||
const { videoProducer, shareProducer } = useMediasoup()
|
|
||||||
const { clients, me } = useClients()
|
|
||||||
|
|
||||||
const gallery = computed(() => {
|
|
||||||
return clients.value.reduce<GalleryItem[]>(
|
|
||||||
(acc, client) => {
|
|
||||||
const { streaming, videoConsumers, shareConsumers } = useClient(client.socketId)
|
|
||||||
|
|
||||||
if (!streaming.value)
|
|
||||||
return acc
|
|
||||||
|
|
||||||
for (const consumer of [...videoConsumers.value, ...shareConsumers.value]) {
|
|
||||||
acc.push({
|
|
||||||
client,
|
|
||||||
stream: new MediaStream([consumer.raw.track]),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
[videoProducer.value, shareProducer.value].reduce<GalleryItem[]>((acc, producer) => {
|
|
||||||
if (!me.value || !producer || !producer.raw.track)
|
|
||||||
return acc
|
|
||||||
|
|
||||||
acc.push({
|
|
||||||
client: me.value,
|
|
||||||
stream: new MediaStream([producer.raw.track]),
|
|
||||||
})
|
|
||||||
|
|
||||||
return acc
|
|
||||||
}, []),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(gallery, (gallery) => {
|
|
||||||
if (gallery.length > 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
navigateTo({ name: 'Index' })
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
option-label="label"
|
option-label="label"
|
||||||
option-value="deviceId"
|
option-value="deviceId"
|
||||||
input-id="inputDevice"
|
input-id="inputDevice"
|
||||||
placeholder="No input device"
|
|
||||||
fluid
|
fluid
|
||||||
:invalid="!inputDeviceExist"
|
:invalid="!inputDeviceExist"
|
||||||
/>
|
/>
|
||||||
@@ -52,36 +51,12 @@
|
|||||||
Video
|
Video
|
||||||
</PrimeDivider>
|
</PrimeDivider>
|
||||||
|
|
||||||
<PrimeFloatLabel variant="on">
|
|
||||||
<PrimeSelect
|
|
||||||
v-model="videoDeviceId"
|
|
||||||
:options="videoInputs"
|
|
||||||
option-label="label"
|
|
||||||
option-value="deviceId"
|
|
||||||
input-id="videoDevice"
|
|
||||||
placeholder="No video device"
|
|
||||||
fluid
|
|
||||||
:invalid="!videoDeviceExist"
|
|
||||||
/>
|
|
||||||
<label for="inputDevice">Input device</label>
|
|
||||||
</PrimeFloatLabel>
|
|
||||||
|
|
||||||
<PrimeDivider align="left">
|
|
||||||
Screen sharing
|
|
||||||
</PrimeDivider>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm mb-2 text-center">
|
<div class="flex justify-between text-sm mb-3">
|
||||||
FPS
|
<span>FPS</span>
|
||||||
</p>
|
<span>{{ shareFps }}</span>
|
||||||
<PrimeSelectButton
|
</div>
|
||||||
v-model="shareFps"
|
<PrimeSlider v-model="shareFps" class="mx-[10px]" :min="30" :max="60" :step="5" />
|
||||||
:options="shareFpsOptions"
|
|
||||||
fluid
|
|
||||||
size="small"
|
|
||||||
option-label="label"
|
|
||||||
option-value="value"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="isTauri">
|
<template v-if="isTauri">
|
||||||
@@ -143,11 +118,10 @@ definePageMeta({
|
|||||||
})
|
})
|
||||||
const { isTauri, version, commitSha } = useApp()
|
const { isTauri, version, commitSha } = useApp()
|
||||||
const { checking, checkForUpdates, lastUpdate } = useUpdater()
|
const { checking, checkForUpdates, lastUpdate } = useUpdater()
|
||||||
const { audioInputs, audioOutputs, videoInputs } = useDevices()
|
const { audioInputs, audioOutputs } = useDevices()
|
||||||
const {
|
const {
|
||||||
inputDeviceId,
|
inputDeviceId,
|
||||||
outputDeviceId,
|
outputDeviceId,
|
||||||
videoDeviceId,
|
|
||||||
autoGainControl,
|
autoGainControl,
|
||||||
noiseSuppression,
|
noiseSuppression,
|
||||||
echoCancellation,
|
echoCancellation,
|
||||||
@@ -155,17 +129,9 @@ const {
|
|||||||
toggleOutputHotkey,
|
toggleOutputHotkey,
|
||||||
inputDeviceExist,
|
inputDeviceExist,
|
||||||
outputDeviceExist,
|
outputDeviceExist,
|
||||||
videoDeviceExist,
|
|
||||||
shareFps,
|
shareFps,
|
||||||
} = usePreferences()
|
} = usePreferences()
|
||||||
|
|
||||||
const shareFpsOptions = [5, 30, 60].map((value) => {
|
|
||||||
return {
|
|
||||||
label: value.toString(),
|
|
||||||
value,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const setupToggleInputHotkey = (event: KeyboardEvent) => setupHotkey(event, toggleInputHotkey)
|
const setupToggleInputHotkey = (event: KeyboardEvent) => setupHotkey(event, toggleInputHotkey)
|
||||||
const setupToggleOutputHotkey = (event: KeyboardEvent) => setupHotkey(event, toggleOutputHotkey)
|
const setupToggleOutputHotkey = (event: KeyboardEvent) => setupHotkey(event, toggleOutputHotkey)
|
||||||
|
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ export default defineNuxtConfig({
|
|||||||
strictPort: true,
|
strictPort: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
// target: 'http://localhost:4000/chad',
|
target: 'http://localhost:4000/chad',
|
||||||
target: 'https://api.koptilnya.xyz/chad',
|
// target: 'https://api.koptilnya.xyz/chad',
|
||||||
ws: true,
|
ws: true,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => {
|
rewrite: (path) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"dev": "nuxt dev --host",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
@@ -19,9 +19,8 @@
|
|||||||
"@tauri-apps/plugin-updater": "~2",
|
"@tauri-apps/plugin-updater": "~2",
|
||||||
"@vueuse/core": "^13.9.0",
|
"@vueuse/core": "^13.9.0",
|
||||||
"hotkeys-js": "^4.0.0",
|
"hotkeys-js": "^4.0.0",
|
||||||
"howler": "^2.2.4",
|
|
||||||
"lucide-vue-next": "^0.562.0",
|
"lucide-vue-next": "^0.562.0",
|
||||||
"mediasoup-client": "^3.18.6",
|
"mediasoup-client": "^3.16.7",
|
||||||
"nuxt": "^4.2.2",
|
"nuxt": "^4.2.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
@@ -38,7 +37,6 @@
|
|||||||
"@antfu/eslint-config": "^5.4.1",
|
"@antfu/eslint-config": "^5.4.1",
|
||||||
"@primevue/nuxt-module": "^4.4.0",
|
"@primevue/nuxt-module": "^4.4.0",
|
||||||
"@tauri-apps/cli": "^2.8.4",
|
"@tauri-apps/cli": "^2.8.4",
|
||||||
"@types/howler": "^2",
|
|
||||||
"eslint": "^9.36.0",
|
"eslint": "^9.36.0",
|
||||||
"eslint-plugin-format": "^1.0.2",
|
"eslint-plugin-format": "^1.0.2",
|
||||||
"sass-embedded": "^1.93.2",
|
"sass-embedded": "^1.93.2",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user