3 Commits

Author SHA1 Message Date
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
1354ca3f7e показывать себя в превью
All checks were successful
Deploy / publish-web (push) Successful in 42s
2026-02-03 16:47:33 +06:00
9 changed files with 92 additions and 40 deletions

View File

@@ -22,6 +22,7 @@ declare module 'vue' {
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']

View File

@@ -111,7 +111,7 @@ const inputMuted = computed(() => {
}) })
const hasBadges = computed(() => { const hasBadges = computed(() => {
return shareConsumers.value.length > 0 return streaming.value
|| premuted.value || premuted.value
|| inputMuted.value || inputMuted.value
|| props.client.outputMuted || props.client.outputMuted

View File

@@ -1,19 +1,38 @@
<template> <template>
<div class="cursor-pointer hover:outline outline-primary rounded overflow-hidden flex items-center justify-center"> <div
<video :srcObject="mediaStream" muted autoplay /> 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.username }}
</PrimeTag>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Consumer } from '#shared/types' import type { ChadClient } from '#shared/types'
const props = defineProps<{ const props = defineProps<{
consumer: Consumer client: ChadClient
stream: MediaStream
}>() }>()
const mediaStream = computed(() => { const { me } = useClients()
return new MediaStream([props.consumer.raw.track]) const fullscreenVideo = useFullscreenVideo()
const isMe = computed(() => {
return props.client.socketId === me.value?.socketId
}) })
function watch() {
fullscreenVideo.show(props.stream)
}
</script> </script>
<style> <style>

View File

@@ -40,7 +40,10 @@ export const useApp = createGlobalState(() => {
}) })
const somebodyStreamingVideo = computed(() => { 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() { async function muteInput() {

View File

@@ -14,10 +14,6 @@ export function useClient(socketId: MaybeRef<ChadClient['socketId']>) {
return Object.values(mediasoup.consumers.value).filter(consumer => consumer.appData.socketId === client.value.socketId) return Object.values(mediasoup.consumers.value).filter(consumer => consumer.appData.socketId === client.value.socketId)
}) })
const producers = computed(() => {
return mediasoup.producers.value.values().filter(producer => producer.appData.socketId === client.value.socketId).toArray()
})
const audioConsumers = computed(() => { const audioConsumers = computed(() => {
return mediasoup.audioConsumers.value.filter(consumer => consumer.appData.socketId === client.value.socketId) return mediasoup.audioConsumers.value.filter(consumer => consumer.appData.socketId === client.value.socketId)
}) })
@@ -30,6 +26,10 @@ export function useClient(socketId: MaybeRef<ChadClient['socketId']>) {
return mediasoup.shareConsumers.value.filter(consumer => consumer.appData.socketId === client.value.socketId) 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(() => { const streaming = computed(() => {
return videoConsumers.value.length > 0 || shareConsumers.value.length > 0 return videoConsumers.value.length > 0 || shareConsumers.value.length > 0
}) })

View File

@@ -384,6 +384,9 @@ export const useMediasoup = createSharedComposable(() => {
const stream = await navigator.mediaDevices.getUserMedia({ const stream = await navigator.mediaDevices.getUserMedia({
video: { video: {
deviceId: { exact: preferences.videoDeviceId.value }, deviceId: { exact: preferences.videoDeviceId.value },
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 60 },
}, },
}) })
@@ -395,9 +398,9 @@ export const useMediasoup = createSharedComposable(() => {
await createProducer({ await createProducer({
track, track,
streamId: 'mic-video', streamId: 'mic-video',
codec: device.value.rtpCapabilities.codecs?.find( // codec: device.value.rtpCapabilities.codecs?.find(
c => c.mimeType.toLowerCase() === 'video/AV1', // c => c.mimeType.toLowerCase() === 'video/AV1',
), // ),
// codecOptions: { // codecOptions: {
// videoGoogleStartBitrate: 1000, // videoGoogleStartBitrate: 1000,
// }, // },

View File

@@ -1,5 +1,5 @@
<template> <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 <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"
> >

View File

@@ -1,38 +1,64 @@
<template> <template>
<div class="grid grid-cols-[1fr_1fr] gap-2"> <div class="grid grid-cols-[1fr_1fr] gap-2">
<template v-for="(group, id) in groupedConsumers" :key="id"> <GalleryCard
<GalleryCard v-for="item in gallery"
v-for="consumer in group" :key="item.client.socketId"
:key="consumer.id" :client="item.client"
:consumer="consumer" :stream="item.stream"
@click="watch(consumer)" />
/>
</template>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Consumer } from '#shared/types' import type { ChadClient } from '#shared/types'
interface GalleryItem {
client: ChadClient
stream: MediaStream
}
definePageMeta({ definePageMeta({
name: 'Gallery', name: 'Gallery',
}) })
const { videoConsumers, shareConsumers } = useMediasoup() const { videoProducer, shareProducer } = useMediasoup()
const fullscreenVideo = useFullscreenVideo() const { clients, me } = useClients()
const groupedConsumers = computed<Partial<Record<string, Consumer[]>>>(() => { const gallery = computed(() => {
if (fullscreenVideo.visible.value) return clients.value.reduce<GalleryItem[]>(
return {} (acc, client) => {
const { streaming, videoConsumers, shareConsumers } = useClient(client.socketId)
const consumers = [...videoConsumers.value, ...shareConsumers.value] if (!streaming.value)
return acc
return Object.groupBy(consumers, (consumer) => { for (const consumer of [...videoConsumers.value, ...shareConsumers.value]) {
return consumer.appData.socketId! 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
}, []),
)
}) })
function watch(consumer: Consumer) { watch(gallery, (gallery) => {
fullscreenVideo.show(new MediaStream([consumer.raw.track])) if (gallery.length > 0)
} return
navigateTo({ name: 'Index' })
})
</script> </script>

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "chad", "productName": "chad",
"version": "0.2.23", "version": "0.2.26",
"identifier": "xyz.koptilnya.chad", "identifier": "xyz.koptilnya.chad",
"build": { "build": {
"frontendDist": "../.output/public", "frontendDist": "../.output/public",
@@ -12,12 +12,12 @@
"app": { "app": {
"windows": [ "windows": [
{ {
"maximizable": false, "maximizable": true,
"label": "main", "label": "main",
"title": "Chad", "title": "Chad",
"width": 800, "width": 800,
"height": 600, "height": 600,
"resizable": false, "resizable": true,
"fullscreen": false, "fullscreen": false,
"center": true, "center": true,
"theme": "Dark", "theme": "Dark",