| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { useMediaStreamElement } from '../../composables/useMediaStreamElement' |
| | | import { UserAvatar } from '../user' |
| | | |
| | | export interface CallParticipantVM { |
| | | userId: number |
| | | nickname: string |
| | | avatar?: string |
| | | isLocal: boolean |
| | | videoStream?: MediaStream | null |
| | | audioStream?: MediaStream | null |
| | | /** çå¾
å å
¥ï¼UI æ¾ç¤ºä¸ç¹å¨ç» */ |
| | | pending?: boolean |
| | | } |
| | | |
| | | const props = defineProps<{ |
| | | participant: CallParticipantVM |
| | | /** æ¬å£°å¨å¼å
³ï¼ä¸º false æ¶éé³è¯¥æ ¼åçè¿ç«¯é³é¢ */ |
| | | speakerEnabled: boolean |
| | | }>() |
| | | |
| | | const setVideoRef = useMediaStreamElement<HTMLVideoElement>(() => props.participant.videoStream) |
| | | const setAudioRef = useMediaStreamElement<HTMLAudioElement>(() => props.participant.audioStream) |
| | | </script> |
| | | |
| | | <template> |
| | | <!-- |
| | | 群éè¯å®«æ ¼éçå个åä¸è
æ ¼åï¼ |
| | | ä¼å
渲æè¿ç«¯è§é¢ï¼æ videoStream æ¶éºæ»¡ï¼ï¼æ è§é¢é级渲æå¤´å + ååè¶åï¼ |
| | | pending=true æ¶å å ãæ¥å
¥ä¸ãä¸ç¹å¨ç»å ä½ï¼ç¨äºè¢«éè¯·äººæªæ¥éåçå 使¸²æï¼ |
| | | è¿ç«¯é³é¢éè¿éè audio ææ¾ï¼æ¬ç«¯ isLocal 䏿¸²æé¿å
åå£°ï¼ |
| | | --> |
| | | <div |
| | | class="flex relative overflow-hidden justify-center items-center w-full h-full rounded-md bg-[#2a2a2c]" |
| | | > |
| | | <!-- è§é¢å¯ç¨ï¼æ¸²æ videoï¼å¦å渲æå¤´åæé»è®¤å ä½ --> |
| | | <video |
| | | v-if="participant.videoStream" |
| | | :ref="setVideoRef" |
| | | class="object-cover w-full h-full" |
| | | autoplay |
| | | playsinline |
| | | :muted="participant.isLocal" |
| | | ></video> |
| | | <div v-else class="flex justify-center items-center w-full h-full"> |
| | | <UserAvatar |
| | | :url="participant.avatar" |
| | | :name="participant.nickname" |
| | | :size="64" |
| | | radius="50%" |
| | | :clickable="false" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- è¿ç«¯é³é¢ï¼éè¿ audio å
ç´ ææ¾ï¼æ¬ç«¯éé³é¿å
åå£°ï¼æ¬å£°å¨å
³éæ¶æ´ä½éé³ --> |
| | | <audio |
| | | v-if="participant.audioStream && !participant.isLocal" |
| | | :ref="setAudioRef" |
| | | autoplay |
| | | :muted="!speakerEnabled" |
| | | ></audio> |
| | | |
| | | <!-- å·¦ä¸è§ååè¶å --> |
| | | <div |
| | | class="flex absolute bottom-3 left-3 gap-1.5 items-center py-[3px] pr-2.5 pl-[3px] text-13px text-white rounded-full bg-black/45 max-w-[calc(100%-60px)]" |
| | | > |
| | | <UserAvatar |
| | | :url="participant.avatar" |
| | | :name="participant.nickname" |
| | | :size="22" |
| | | radius="50%" |
| | | :clickable="false" |
| | | /> |
| | | <span class="truncate min-w-0">{{ participant.nickname }}</span> |
| | | </div> |
| | | |
| | | <!-- çå¾
对æ¹å å
¥ï¼ä¸ç¹å¨ç» +ãæ¥å
¥ä¸ãææ¡ï¼ä½ç½®å¨æ ¼åä¸å¿ä¸æ¹ 60px --> |
| | | <div |
| | | v-if="participant.pending" |
| | | class="flex absolute left-1/2 flex-col gap-1.5 items-center -translate-x-1/2 -translate-y-1/2 top-[calc(50%+60px)]" |
| | | > |
| | | <div class="flex gap-1.5"> |
| | | <span |
| | | v-for="i in 3" |
| | | :key="i" |
| | | class="tile-dot w-1.5 h-1.5 rounded-full bg-white/60" |
| | | :style="{ animationDelay: `${(i - 1) * 0.2}s` }" |
| | | ></span> |
| | | </div> |
| | | <span class="text-xs text-white/70">æ¥å
¥ä¸</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | /* ä¸ç¹æ·¡å
¥æ·¡åºå¨ç»ï¼@keyframes å¿
é¡» CSS å®ä¹ */ |
| | | .tile-dot { |
| | | animation: tile-dot 1.4s infinite ease-in-out both; |
| | | } |
| | | @keyframes tile-dot { |
| | | 0%, |
| | | 80%, |
| | | 100% { |
| | | opacity: 0.3; |
| | | } |
| | | 40% { |
| | | opacity: 1; |
| | | } |
| | | } |
| | | </style> |