diff --git a/src/app/listener.middleware/handler.footprint.ts b/src/app/listener.middleware/handler.footprint.ts index 11f07162..c188c361 100644 --- a/src/app/listener.middleware/handler.footprint.ts +++ b/src/app/listener.middleware/handler.footprint.ts @@ -5,6 +5,7 @@ interface Params { data: any; operation: string; } +const ignores = ["voice_fullscreen", "voice"]; export default async function handler({ operation, data = {}, payload }: Params) { const table = window.CACHE["footprint"] as typeof localforage;; if (operation.startsWith("reset")) { @@ -71,14 +72,14 @@ export default async function handler({ operation, data = {}, payload }: Params) break; case "updateChannelVisibleAside": { - if (payload.aside !== "voice") { + if (!ignores.includes(payload.aside)) { await table?.setItem("channelAsides", data.channelAsides); } } break; case "updateDMVisibleAside": { - if (payload.aside !== "voice") { + if (!ignores.includes(payload.aside)) { await table?.setItem("dmAsides", data.dmAsides); } } diff --git a/src/app/slices/footprint.ts b/src/app/slices/footprint.ts index 0241d64a..644e8de6 100644 --- a/src/app/slices/footprint.ts +++ b/src/app/slices/footprint.ts @@ -4,7 +4,7 @@ import { OG } from "../../types/resource"; import { AutoDeleteMessageSettingDTO, AutoDeleteMsgForGroup, AutoDeleteMsgForUser, AutoDeleteSettingForChannels, AutoDeleteSettingForUsers } from "../../types/sse"; import { resetAuthData } from "./auth.data"; -type ChannelAside = "members" | "voice" | null; +type ChannelAside = "members" | "voice" | "voice_fullscreen" | null; type DMAside = "voice" | null; export interface State { og: { [url: string]: OG } diff --git a/src/common/component/User/index.tsx b/src/common/component/User/index.tsx index 88f64165..91632443 100644 --- a/src/common/component/User/index.tsx +++ b/src/common/component/User/index.tsx @@ -72,7 +72,7 @@ const User: FC = ({ name={curr.name} alt="avatar" /> - {curr.is_bot ? : statusElement} + {curr.is_bot ? : statusElement} {!compact && ( diff --git a/src/common/component/Voice.tsx b/src/common/component/Voice.tsx index 0a0cabf4..78a35b9c 100644 --- a/src/common/component/Voice.tsx +++ b/src/common/component/Voice.tsx @@ -36,6 +36,17 @@ const Voice = () => { window.VOICE_TRACK_MAP[+user.uid] = user.audioTrack; } if (mediaType == "video") { + const label = user.videoTrack?.getMediaStreamTrack()?.label; + console.log("labell", label); + + if (label?.includes("screen")) { + // 远端用户共享屏幕 + // const screenTrack = user.videoTrack as ShareScreenTrack; + // screenTrack.on("track-ended", () => { + // // 远端用户停止共享屏幕 + dispatch(updateVoicingMember({ uid: +user.uid, info: { shareScreen: true } })); + } + window.VIDEO_TRACK_MAP[+user.uid] = user.videoTrack; playAgoraVideo(+user.uid); } @@ -46,7 +57,9 @@ const Voice = () => { } if (!user.hasVideo) { // 远端用户取消了视频 - dispatch(updateVoicingMember({ uid: +user.uid, info: { video: false } })); + dispatch(updateVoicingMember({ uid: +user.uid, info: { video: false, shareScreen: false } })); + // 注销本地视频变量 + window.VIDEO_TRACK_MAP[+user.uid] = null; // 关闭视频后,需要把视频的高度设置回去 const playerEle = document.querySelector(`#CAMERA_${user.uid}`) as HTMLElement; playerEle.classList.remove("h-[120px]"); @@ -216,30 +229,32 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { } }; const stopShareScreen = async () => { - const localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ShareScreenTrack; - if (localVideoTrack) { - await window.VOICE_CLIENT?.unpublish(localVideoTrack); - if ("close" in localVideoTrack) { - localVideoTrack.close(); - } else { - localVideoTrack[0].close(); - } - // localVideoTrack.close(); - window.VIDEO_TRACK_MAP[loginUid] = null; - // 关闭视频后,需要把视频的高度设置回去 - const playerEle = document.querySelector(`#CAMERA_${loginUid}`) as HTMLElement; - playerEle.classList.remove("h-[120px]"); - dispatch(updateVoicingInfo({ - video: false, - shareScreen: false, - id, - context, - })); + let localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ShareScreenTrack | null; + if (!localVideoTrack) return; + await window.VOICE_CLIENT?.unpublish(localVideoTrack); + if ("close" in localVideoTrack) { + localVideoTrack.close(); + } else { + localVideoTrack[0].close(); + // localVideoTrack[0]=null } + // localVideoTrack.close(); + window.VIDEO_TRACK_MAP[loginUid] = null; + // 关闭视频后,需要把视频的高度设置回去 + const playerEle = document.querySelector(`#CAMERA_${loginUid}`) as HTMLElement; + playerEle.classList.remove("h-[120px]"); + dispatch(updateVoicingInfo({ + video: false, + shareScreen: false, + id, + context, + })); }; const startShareScreen = async () => { try { const localVideoTrack = await AgoraRTC.createScreenVideoTrack({ + electronScreenSourceId: "share_screen", + selfBrowserSurface: "exclude", // 配置屏幕共享编码参数,详情请查看 API 文档。 encoderConfig: "1080p_1", // 设置视频传输优化策略为清晰优先或流畅优先。 diff --git a/src/routes/chat/RTCWidget.tsx b/src/routes/chat/RTCWidget.tsx index 7da11dac..a7a71116 100644 --- a/src/routes/chat/RTCWidget.tsx +++ b/src/routes/chat/RTCWidget.tsx @@ -14,10 +14,11 @@ import IconScreen from '../../assets/icons/share.screen.svg'; import { useVoice } from '../../common/component/Voice'; import Signal from '../../common/component/Signal'; import Tooltip from '../../common/component/Tooltip'; +import { ChatContext } from '../../types/common'; type Props = { id: number, - context?: "channel" | "dm" + context?: ChatContext } const RTCWidget = ({ id, context = "channel" }: Props) => { diff --git a/src/routes/chat/SessionList/ContextMenu.tsx b/src/routes/chat/SessionList/ContextMenu.tsx index 42a09e52..b9320f41 100644 --- a/src/routes/chat/SessionList/ContextMenu.tsx +++ b/src/routes/chat/SessionList/ContextMenu.tsx @@ -9,8 +9,9 @@ import ContextMenu, { Item } from "../../../common/component/ContextMenu"; import useUserOperation from "../../../common/hook/useUserOperation"; import { useAppSelector } from "../../../app/store"; import { useTranslation } from "react-i18next"; +import { ChatContext } from "../../../types/common"; type Props = { - context: "dm" | "channel"; + context: ChatContext; id: number; visible: boolean; mid: number; diff --git a/src/routes/chat/SessionList/Session.tsx b/src/routes/chat/SessionList/Session.tsx index 1465b743..89fc63ed 100644 --- a/src/routes/chat/SessionList/Session.tsx +++ b/src/routes/chat/SessionList/Session.tsx @@ -14,9 +14,10 @@ import useContextMenu from "../../../common/hook/useContextMenu"; import useUploadFile from "../../../common/hook/useUploadFile"; import { useAppSelector } from "../../../app/store"; import { fromNowTime } from "../../../common/utils"; +import { ChatContext } from "../../../types/common"; interface IProps { - type?: "dm" | "channel"; + type?: ChatContext; id: number; mid: number; setDeleteChannelId: (param: number) => void; diff --git a/src/routes/chat/SessionList/index.tsx b/src/routes/chat/SessionList/index.tsx index 193b95a2..cf312a1a 100644 --- a/src/routes/chat/SessionList/index.tsx +++ b/src/routes/chat/SessionList/index.tsx @@ -5,8 +5,9 @@ import Session from "./Session"; import DeleteChannelConfirmModal from "../../settingChannel/DeleteConfirmModal"; import InviteModal from "../../../common/component/InviteModal"; import { useAppSelector } from "../../../app/store"; +import { ChatContext } from "../../../types/common"; export interface ChatSession { - type: "dm" | "channel"; + type: ChatContext; id: number; mid: number; unread: number; diff --git a/src/routes/chat/VoiceChat/Dashboard.tsx b/src/routes/chat/VoiceChat/Dashboard.tsx index 7c453905..cf7efaaa 100644 --- a/src/routes/chat/VoiceChat/Dashboard.tsx +++ b/src/routes/chat/VoiceChat/Dashboard.tsx @@ -2,20 +2,21 @@ // import { useDispatch } from 'react-redux'; import VoiceManagement from './VoiceManagement'; import { useVoice } from '../../../common/component/Voice'; +import { ChatContext } from '../../../types/common'; type Props = { visible: boolean, - context?: "channel" | "dm", + context?: ChatContext, id: number, } const Dashboard = ({ context = "channel", id, visible }: Props) => { - const { voicingInfo, setMute, setDeafen, leave, closeCamera, openCamera, startShareScreen, stopShareScreen } = useVoice({ id, context }); + const { voicingInfo, setMute, leave, closeCamera, openCamera, startShareScreen, stopShareScreen } = useVoice({ id, context }); // const dispatch = useDispatch(); return
- +
; }; diff --git a/src/routes/chat/VoiceChat/VoiceManagement.tsx b/src/routes/chat/VoiceChat/VoiceManagement.tsx index c7f3677c..2c0a1252 100644 --- a/src/routes/chat/VoiceChat/VoiceManagement.tsx +++ b/src/routes/chat/VoiceChat/VoiceManagement.tsx @@ -5,23 +5,28 @@ import { useTranslation } from 'react-i18next'; import { VoicingInfo } from '../../../app/slices/voice'; import { useAppSelector } from '../../../app/store'; import Avatar from '../../../common/component/Avatar'; -import IconHeadphone from '../../../assets/icons/sound.on.svg'; -import IconHeadphoneOff from '../../../assets/icons/sound.off.svg'; +// import IconHeadphone from '../../../assets/icons/sound.on.svg'; +// import IconHeadphoneOff from '../../../assets/icons/sound.off.svg'; import IconMic from '../../../assets/icons/mic.on.svg'; import IconMicOff from '../../../assets/icons/mic.off.svg'; import IconCameraOff from '../../../assets/icons/camera.off.svg'; import IconCamera from '../../../assets/icons/camera.svg'; +import IconFullscreen from '../../../assets/icons/fullscreen.svg'; import IconScreen from '../../../assets/icons/share.screen.svg'; -import StyledButton from '../../../common/component/styled/Button'; import IconCallOff from '../../../assets/icons/call.off.svg'; +import StyledButton from '../../../common/component/styled/Button'; import Tooltip from '../../../common/component/Tooltip'; import { playAgoraVideo } from '../../../common/utils'; +import { ChatContext } from '../../../types/common'; +import { updateChannelVisibleAside } from '../../../app/slices/footprint'; +import { useDispatch } from 'react-redux'; // import User from '../../../common/component/User'; type Props = { + id: number, + context: ChatContext, info: VoicingInfo | null, setMute: (param: boolean) => void, - setDeafen: (param: boolean) => void, leave: () => void, closeCamera: () => void, openCamera: () => void, @@ -29,8 +34,9 @@ type Props = { stopShareScreen: () => void } -const VoiceManagement = ({ info, setMute, setDeafen, leave, closeCamera, openCamera, startShareScreen, stopShareScreen }: Props) => { +const VoiceManagement = ({ id, context, info, setMute, leave, closeCamera, openCamera, startShareScreen, stopShareScreen }: Props) => { const { t } = useTranslation("chat"); + const dispatch = useDispatch(); const { userData, voicingMembers } = useAppSelector(store => { return { userData: store.users.byId, @@ -43,9 +49,13 @@ const VoiceManagement = ({ info, setMute, setDeafen, leave, closeCamera, openCam playAgoraVideo(id); }); }, [voicingMembers.ids]); - + const handleFullscreen = () => { + if (context == "channel") { + dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" })); + } + }; if (!info) return null; - const { deafen, muted, video, shareScreen } = info; + const { muted, video, shareScreen } = info; const nameClass = clsx(`text-sm text-gray-500 max-w-[120px] truncate font-semibold dark:text-white`); const members = voicingMembers.ids; const membersData = voicingMembers.byId; @@ -102,11 +112,6 @@ const VoiceManagement = ({ info, setMute, setDeafen, leave, closeCamera, openCam
    - -
  • - {deafen ? : } -
  • -
  • {muted ? : } @@ -122,6 +127,11 @@ const VoiceManagement = ({ info, setMute, setDeafen, leave, closeCamera, openCam
  • + +
  • + +
  • +
diff --git a/src/routes/chat/VoiceChat/index.tsx b/src/routes/chat/VoiceChat/index.tsx index 1d4af9f9..81e3350c 100644 --- a/src/routes/chat/VoiceChat/index.tsx +++ b/src/routes/chat/VoiceChat/index.tsx @@ -9,9 +9,10 @@ import IconHeadphone from '../../../assets/icons/headphone.svg'; import Tooltip from '../../../common/component/Tooltip'; import { useVoice } from '../../../common/component/Voice'; import { useGetAgoraStatusQuery } from '../../../app/services/server'; +import { ChatContext } from '../../../types/common'; type Props = { - context?: "channel" | "dm" + context?: ChatContext id: number, } diff --git a/src/routes/chat/VoiceFullscreen.tsx b/src/routes/chat/VoiceFullscreen.tsx new file mode 100644 index 00000000..58d22437 --- /dev/null +++ b/src/routes/chat/VoiceFullscreen.tsx @@ -0,0 +1,145 @@ +import { useEffect, useState, MouseEvent } from 'react'; +import { useDispatch } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import clsx from 'clsx'; +import IconMic from '../../assets/icons/mic.on.svg'; +import IconMicOff from '../../assets/icons/mic.off.svg'; +import IconCameraOff from '../../assets/icons/camera.off.svg'; +import IconPin from '../../assets/icons/pin.svg'; +import IconCamera from '../../assets/icons/camera.svg'; +import IconScreen from '../../assets/icons/share.screen.svg'; +import IconExitScreen from '../../assets/icons/fullscreen.exit.svg'; +import IconCallOff from '../../assets/icons/call.off.svg'; +import { ChatContext } from '../../types/common'; +import { useAppSelector } from '../../app/store'; +import Avatar from '../../common/component/Avatar'; +import { playAgoraVideo } from '../../common/utils'; +import { updateChannelVisibleAside } from '../../app/slices/footprint'; +import Tooltip from '../../common/component/Tooltip'; +import { useVoice } from '../../common/component/Voice'; +import StyledButton from '../../common/component/styled/Button'; + +type Props = { + context: ChatContext, + id: number +} + +const VoiceFullscreen = ({ id, context }: Props) => { + const [speakingIndex, setSpeakingIndex] = useState(0); + const [pin, setPin] = useState(undefined); + const dispatch = useDispatch(); + const { t } = useTranslation("chat"); + const { voicingInfo, setMute, leave, closeCamera, openCamera, startShareScreen, stopShareScreen } = useVoice({ id, context }); + const { name, userData, voicingMembers } = useAppSelector(store => { + return { + userData: store.users.byId, + voicingMembers: store.voice.voicingMembers, + name: context == "channel" ? store.channels.byId[id].name : store.users.byId[id].name + }; + }); + useEffect(() => { + const ids = voicingMembers.ids; + ids.forEach((id, idx) => { + playAgoraVideo(id); + const { speakingVolume = 0 } = voicingMembers.byId[id]; + const speaking = speakingVolume > 50; + if (speaking) { + setSpeakingIndex(idx); + } + }); + + }, [voicingMembers]); + const handleExitFullscreen = () => { + if (context == "channel") { + dispatch(updateChannelVisibleAside({ id, aside: "voice" })); + } + }; + const handlePin = (evt: MouseEvent) => { + const idx = evt.currentTarget.dataset.idx ?? ""; + if (idx == "undefined") { + setPin(undefined); + } else { + setPin(parseInt(idx)); + } + }; + if (!voicingInfo) return null; + const _name = context == "channel" ? `# ${name}` : `@ ${name}`; + const members = voicingMembers.ids; + const membersData = voicingMembers.byId; + const hasPin = typeof pin !== "undefined"; + const { muted, video, shareScreen } = voicingInfo; + return ( +
+ {/* top */} +
+ {_name} +
+ +
+
+ {/* middle */} +
    + {members.map((uid, idx) => { + const curr = userData[uid]; + if (!curr) return null; + const { muted, speakingVolume = 0, shareScreen } = membersData[uid]; + const speaking = speakingVolume > 50; + const special = hasPin ? pin == idx : idx == speakingIndex; + const disablePin = special && !hasPin; + return
  • +
    + {speaking &&
    } + +
    + {shareScreen ?
    : null} + {!disablePin && } + + {curr?.name} + +
    + {muted ? : } +
    +
    + {/* camera placeholder */} +
    +
  • ; + })} + +
+ {/* bottom */} +
    + +
  • + {muted ? : } +
  • +
    + +
  • + {video ? : } +
  • +
    + +
  • + +
  • +
    + + + + + +
+
+ ); +}; + +export default VoiceFullscreen; \ No newline at end of file diff --git a/src/routes/chat/index.tsx b/src/routes/chat/index.tsx index c615cbce..32b958f4 100644 --- a/src/routes/chat/index.tsx +++ b/src/routes/chat/index.tsx @@ -15,6 +15,7 @@ import GuestChannelChat from "./GuestChannelChat"; import GuestSessionList from "./GuestSessionList"; import ErrorCatcher from "../../common/component/ErrorCatcher"; import RTCWidget from "./RTCWidget"; +import VoiceFullscreen from "./VoiceFullscreen"; function ChatPage() { const isHomePath = useMatch(`/`); @@ -23,10 +24,11 @@ function ChatPage() { const [channelModalVisible, setChannelModalVisible] = useState(false); const [usersModalVisible, setUsersModalVisible] = useState(false); const { channel_id = 0, user_id = 0 } = useParams(); - const { sessionUids, isGuest } = useAppSelector((store) => { + const { sessionUids, isGuest, aside = "" } = useAppSelector((store) => { return { isGuest: store.authData.guest, - sessionUids: store.userMessage.ids + sessionUids: store.userMessage.ids, + aside: channel_id ? store.footprint.channelAsides[+channel_id] : store.footprint.dmAsides[+user_id] }; }); const toggleUsersModalVisible = () => { @@ -49,9 +51,14 @@ function ChatPage() { : undefined; // console.log("temp uid", tmpUid); const placeholderVisible = channel_id == 0 && user_id == 0; + const voiceFullscreenVisible = aside === "voice_fullscreen"; + const channelChatVisible = channel_id != 0 && aside !== "voice_fullscreen"; + const dmChatVisible = user_id != 0 && aside !== "voice_fullscreen"; const isMainPath = isHomePath || isChatHomePath; const context = channel_id !== 0 ? "channel" : "dm"; - const contextId = (channel_id || user_id) ?? 0; + const contextId = (+channel_id || +user_id) ?? 0; + console.log("fffff", channel_id, user_id, aside, channelChatVisible); + return ( {channelModalVisible && ( @@ -71,14 +78,15 @@ function ChatPage() { : }
+ {voiceFullscreenVisible && } {placeholderVisible && (isGuest ? : )} - {channel_id !== 0 && + {channelChatVisible && (isGuest ? ( ) : ( ))} - {user_id !== 0 && } + {dmChatVisible && }
diff --git a/src/types/common.ts b/src/types/common.ts index 38d2799b..b09aba5c 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1,6 +1,7 @@ export interface EntityId { id: number; } +export type ChatContext = "dm" | "channel"; export type Theme = "auto" | "dark" | "light"; export type AuthType = "register" | "login"; export type PriceType = "subscription" | "payment" | "booking";