RtcLayout.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import {
  2. RoomAudioRenderer,
  3. TrackLoop,
  4. TrackRefContext,
  5. useConnectionState,
  6. useTracks,
  7. VideoTrack,
  8. } from "@livekit/components-react";
  9. import { PublicUserItem } from "@openim/wasm-client-sdk/lib/types/entity";
  10. import { Spin } from "antd";
  11. import clsx from "clsx";
  12. import {
  13. ConnectionState,
  14. LocalParticipant,
  15. Participant,
  16. ParticipantEvent,
  17. Track,
  18. } from "livekit-client";
  19. import { useEffect, useRef, useState } from "react";
  20. import callingMp3 from "@/assets/audios/calling.mp3";
  21. import OIMAvatar from "@/components/OIMAvatar";
  22. import { CustomType } from "@/constants";
  23. import { AuthData, InviteData } from "./data";
  24. import { RtcControl } from "./RtcControl";
  25. const localVideoClasses =
  26. "absolute right-3 top-3 !w-[100px] !h-[150px] rounded-md z-10";
  27. const remoteVideoClasses = "absolute top-0 z-0";
  28. interface IRtcLayoutProps {
  29. connect: boolean;
  30. isConnected: boolean;
  31. isRecv: boolean;
  32. inviteData?: InviteData;
  33. closeOverlay: () => void;
  34. sendCustomSignal: (recvID: string, customType: CustomType) => Promise<void>;
  35. connectRtc: (data?: AuthData) => void;
  36. }
  37. export const RtcLayout = ({
  38. connect,
  39. isConnected,
  40. isRecv,
  41. inviteData,
  42. connectRtc,
  43. sendCustomSignal,
  44. closeOverlay,
  45. }: IRtcLayoutProps) => {
  46. const isVideoCall = inviteData?.invitation?.mediaType === "video";
  47. const tracks = useTracks([Track.Source.Camera]);
  48. const remoteParticipant = tracks.find((track) => !isLocal(track.participant));
  49. const isWaiting = !connect && !isConnected;
  50. const [isRemoteVideoMuted, setIsRemoteVideoMuted] = useState(false);
  51. const callingRef = useRef<HTMLAudioElement>(null);
  52. const connectState = useConnectionState();
  53. useEffect(() => {
  54. if (!remoteParticipant?.participant.identity) return;
  55. const trackMuteUpdate = () => {
  56. setIsRemoteVideoMuted(!remoteParticipant?.participant.isCameraEnabled);
  57. };
  58. remoteParticipant?.participant.on(ParticipantEvent.TrackMuted, trackMuteUpdate);
  59. remoteParticipant?.participant.on(ParticipantEvent.TrackUnmuted, trackMuteUpdate);
  60. trackMuteUpdate();
  61. }, [remoteParticipant?.participant.identity]);
  62. const renderContent = () => {
  63. if (!isWaiting && isVideoCall && !isRemoteVideoMuted) return null;
  64. return (
  65. <SingleProfile
  66. isWaiting={isWaiting}
  67. userInfo={inviteData?.participant?.userInfo}
  68. />
  69. );
  70. };
  71. useEffect(() => {
  72. if (isWaiting) {
  73. handlePlay();
  74. } else {
  75. handlePause();
  76. }
  77. }, [isWaiting]);
  78. const handlePlay = () => {
  79. if (callingRef.current) {
  80. callingRef.current.play();
  81. }
  82. };
  83. const handlePause = () => {
  84. if (callingRef.current) {
  85. callingRef.current.pause();
  86. }
  87. };
  88. return (
  89. <Spin spinning={connectState === ConnectionState.Connecting}>
  90. <div
  91. className="relative"
  92. style={{
  93. height: `340px`,
  94. width: `480px`,
  95. }}
  96. >
  97. <div
  98. className={clsx(
  99. "flex h-full flex-col items-center justify-between bg-[#262729]",
  100. { "!bg-[#F2F8FF]": isWaiting },
  101. )}
  102. >
  103. {renderContent()}
  104. <RtcControl
  105. isWaiting={isWaiting}
  106. isRecv={isRecv}
  107. isConnected={isConnected}
  108. // @ts-ignore
  109. invitation={inviteData?.invitation}
  110. closeOverlay={closeOverlay}
  111. connectRtc={connectRtc}
  112. sendCustomSignal={sendCustomSignal}
  113. />
  114. </div>
  115. {isConnected && (
  116. <TrackLoop tracks={tracks}>
  117. <TrackRefContext.Consumer>
  118. {(track) =>
  119. track && (
  120. <VideoTrack
  121. {...track}
  122. className={
  123. isLocal(track.participant)
  124. ? localVideoClasses
  125. : `${remoteVideoClasses} ${isRemoteVideoMuted ? "hidden" : ""}`
  126. }
  127. />
  128. )
  129. }
  130. </TrackRefContext.Consumer>
  131. </TrackLoop>
  132. )}
  133. </div>
  134. <RoomAudioRenderer />
  135. <audio controls={false} ref={callingRef} src={callingMp3} loop={true} />
  136. </Spin>
  137. );
  138. };
  139. interface ISingleProfileProps {
  140. isWaiting: boolean;
  141. userInfo?: PublicUserItem;
  142. }
  143. const SingleProfile = ({ isWaiting, userInfo }: ISingleProfileProps) => {
  144. return (
  145. <div className="absolute top-[10%] flex flex-col items-center">
  146. <OIMAvatar size={48} src={userInfo?.faceURL} text={userInfo?.nickname} />
  147. <div
  148. className={clsx("mt-3 max-w-[120px] truncate text-white", {
  149. "!text-[var(--base-black)]": isWaiting,
  150. })}
  151. >
  152. {userInfo?.nickname}
  153. </div>
  154. </div>
  155. );
  156. };
  157. const isLocal = (p: Participant) => {
  158. return p instanceof LocalParticipant;
  159. };