6 Commits

Author SHA1 Message Date
a37b2048fe cringe sfx
All checks were successful
Deploy / publish-web (push) Successful in 53s
2026-02-06 22:41:58 +06:00
e3ac3e003c cringe sfx
All checks were successful
Deploy / publish-web (push) Successful in 2m35s
2026-02-06 22:32:01 +06:00
6fa142f133 productName typo 2026-02-03 22:06:09 +06:00
8e0a08da05 icons
All checks were successful
Deploy / publish-web (push) Successful in 44s
2026-02-03 22:02:45 +06:00
0a3b2c3dc8 resize
All checks were successful
Deploy / publish-web (push) Successful in 44s
2026-02-03 17:05:43 +06:00
e5f1e6bbb3 показывать себя в превью
All checks were successful
Deploy / publish-web (push) Successful in 41s
2026-02-03 16:54:51 +06:00
218 changed files with 112 additions and 22 deletions

Binary file not shown.

View File

@@ -1,22 +1,16 @@
<template>
<div
class="relative rounded overflow-hidden flex items-center justify-center"
:class="{
'group cursor-pointer hover:outline outline-primary': !isMe,
}"
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"
:class="{
'group-hover:opacity-100': !isMe,
}"
class="absolute bottom-2 text-sm opacity-70 group-hover:opacity-100"
rounded
>
{{ isMe ? 'You' : client.username }}
{{ isMe ? 'You' : client.displayName }}
</PrimeTag>
</div>
</template>
@@ -37,9 +31,6 @@ const isMe = computed(() => {
})
function watch() {
if (isMe.value)
return
fullscreenVideo.show(props.stream)
}
</script>

View File

@@ -8,6 +8,7 @@ export const useApp = createGlobalState(() => {
const mediasoup = useMediasoup()
const signaling = useSignaling()
const toast = useToast()
const sfx = useSfx()
const ready = ref(false)
const isTauri = computed(() => '__TAURI_INTERNALS__' in window)
@@ -40,7 +41,10 @@ export const useApp = createGlobalState(() => {
})
const somebodyStreamingVideo = computed(() => {
return mediasoup.videoConsumers.value.length > 0 || mediasoup.shareConsumers.value.length > 0
return !!mediasoup.videoProducer.value
|| !!mediasoup.shareProducer.value
|| mediasoup.videoConsumers.value.length > 0
|| mediasoup.shareConsumers.value.length > 0
})
async function muteInput() {
@@ -49,6 +53,7 @@ export const useApp = createGlobalState(() => {
await mediasoup.pauseProducer(mediasoup.micProducer.value)
sfx.play('/sfx/off_micr.ogg').then()
toast.add({ severity: 'info', summary: 'Microphone muted', closable: false, life: 1000 })
}
@@ -62,6 +67,7 @@ export const useApp = createGlobalState(() => {
await mediasoup.resumeProducer(mediasoup.micProducer.value)
sfx.play('/sfx/on_micr.ogg').then()
toast.add({ severity: 'info', summary: 'Microphone activated', closable: false, life: 1000 })
}
@@ -112,18 +118,22 @@ export const useApp = createGlobalState(() => {
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() {
if (!mediasoup.shareProducer.value) {
await mediasoup.enableShare()
await sfx.play('/sfx/on_trans.ogg', 0.03)
}
else {
await mediasoup.disableProducer(mediasoup.shareProducer.value)
await sfx.play('/sfx/off_trans.ogg', 0.03)
}
}

View File

@@ -29,6 +29,7 @@ const ICE_SERVERS: RTCIceServer[] = [
export const useMediasoup = createSharedComposable(() => {
const toast = useToast()
const sfx = useSfx()
const signaling = useSignaling()
const { addClient, removeClient } = useClients()
@@ -175,6 +176,7 @@ export const useMediasoup = createSharedComposable(() => {
})
socket.on('newPeer', (client) => {
sfx.playRandomConnectionSound(client.socketId).then()
addClient(client)
})
@@ -200,6 +202,9 @@ export const useMediasoup = createSharedComposable(() => {
appData: { ...appData, socketId },
})
if (kind === 'video')
sfx.play('/sfx/on_trans.ogg', 0.03).then()
if (producerPaused)
consumer.pause()

View File

@@ -0,0 +1,43 @@
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,
}
})

View File

@@ -1,5 +1,5 @@
<template>
<div class="grid grid-cols-[1fr_1.25fr] gap-2 p-2 h-screen grid-rows-[auto_1fr] max-w-full">
<div class="grid grid-cols-[360px_1fr] gap-2 p-2 h-screen grid-rows-[auto_1fr] max-w-full">
<div
class="flex items-center justify-between gap-2 rounded-xl p-3 bg-surface-950"
>
@@ -40,7 +40,7 @@
<PrimeSelectButton
v-model="activeTab"
:options="tabs"
data-key="id"
option-label="id"
:allow-empty="false"
style="--p-togglebutton-content-padding: 0.25rem 0.5rem"
>

View File

@@ -5,7 +5,6 @@
:key="item.client.socketId"
:client="item.client"
:stream="item.stream"
@click="watch(item.stream)"
/>
</div>
</template>

View File

@@ -74,7 +74,14 @@
<p class="text-sm mb-2 text-center">
FPS
</p>
<PrimeSelectButton v-model="shareFps" :options="[5, 30, 60]" fluid size="small" />
<PrimeSelectButton
v-model="shareFps"
:options="shareFpsOptions"
fluid
size="small"
option-label="label"
option-value="value"
/>
</div>
<template v-if="isTauri">
@@ -152,6 +159,13 @@ const {
shareFps,
} = usePreferences()
const shareFpsOptions = [5, 30, 60].map((value) => {
return {
label: value.toString(),
value,
}
})
const setupToggleInputHotkey = (event: KeyboardEvent) => setupHotkey(event, toggleInputHotkey)
const setupToggleOutputHotkey = (event: KeyboardEvent) => setupHotkey(event, toggleOutputHotkey)

View File

@@ -19,6 +19,7 @@
"@tauri-apps/plugin-updater": "~2",
"@vueuse/core": "^13.9.0",
"hotkeys-js": "^4.0.0",
"howler": "^2.2.4",
"lucide-vue-next": "^0.562.0",
"mediasoup-client": "^3.18.6",
"nuxt": "^4.2.2",
@@ -37,6 +38,7 @@
"@antfu/eslint-config": "^5.4.1",
"@primevue/nuxt-module": "^4.4.0",
"@tauri-apps/cli": "^2.8.4",
"@types/howler": "^2",
"eslint": "^9.36.0",
"eslint-plugin-format": "^1.0.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.

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