index.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. import { makeStyles, Typography } from '@material-ui/core'
  2. import { useState,useEffect,useCallback,useRef } from 'react';
  3. import { useSelector } from 'react-redux';
  4. import io from 'socket.io-client';
  5. import Moveable from "react-moveable";
  6. import { OnDrag } from "react-moveable";
  7. import ListItemText from '@mui/material/ListItemText';
  8. import ListItemAvatar from '@mui/material/ListItemAvatar';
  9. import Avatar from '@mui/material/Avatar';
  10. import PhoneIcon from '@mui/icons-material/Phone';
  11. import MinimizeIcon from '@mui/icons-material/Minimize';
  12. import CropLandscapeIcon from '@mui/icons-material/CropLandscape';
  13. import CloseIcon from '@mui/icons-material/Close';
  14. import VideocamIcon from '@mui/icons-material/Videocam';
  15. import VideocamOffIcon from '@mui/icons-material/VideocamOff';
  16. import MicIcon from '@mui/icons-material/Mic';
  17. import MicOffIcon from '@mui/icons-material/MicOff';
  18. import CallEndIcon from '@mui/icons-material/CallEnd';
  19. import { getChat } from '../../../redux/chat/selector';
  20. import { getAuthorizationState } from '../../../redux/authorization/selector';
  21. import { prodAwsS3,prodSocketURL, firstLetter, slicedWord,getTimeBySeconds } from '../../../helpers'
  22. import { socketIdChat } from '../../../api-data';
  23. const Peer = require('simple-peer')
  24. const socket = io(prodSocketURL)
  25. const useStyles = makeStyles({
  26. container: {
  27. position: 'absolute',
  28. left: 0,
  29. top: 0,
  30. width: '100vw',
  31. height:'100vh',
  32. overflow: 'hidden',
  33. zIndex:100,
  34. display: 'flex',
  35. justifyContent: 'center',
  36. alignItems: 'center',
  37. alignContent: "center",
  38. backgroundColor: 'rgba(104, 105, 104, 0.6)',
  39. },
  40. modalCall: {
  41. background: 'rgb(36, 36, 36)',
  42. position:'relative',
  43. display: 'flex',
  44. flexDirection: 'column',
  45. justifyContent: 'start',
  46. alignItems: 'center',
  47. justifyItems:"center",
  48. borderRadius: 7,
  49. },
  50. rightIcons: {
  51. display: 'flex',
  52. justifyContent: 'end',
  53. alignContent: 'center',
  54. alignItems: 'center',
  55. width: '100%',
  56. position: 'absolute',
  57. left: 0,
  58. top: 0,
  59. zIndex:1
  60. },
  61. rightIconWrapper: {
  62. color: '#ffffff',
  63. cursor: 'pointer',
  64. padding:'3px 10px 3px 10px',
  65. },
  66. rightIconWrapperClose: {
  67. color: '#ffffff',
  68. cursor: 'pointer',
  69. padding:'3px 10px 3px 10px',
  70. borderTopRightRadius:7,
  71. '&:hover': {
  72. backgroundColor:'#f02a2a'
  73. }
  74. },
  75. bottomWrapper: {
  76. display: 'flex',
  77. justifyContent: 'center',
  78. padding: 5,
  79. position: "absolute",
  80. left: 0,
  81. bottom: 0,
  82. width:'100%'
  83. },
  84. bottomItem: {
  85. display: 'flex',
  86. flexDirection: 'column',
  87. justifyContent: 'center',
  88. alignContent: 'center',
  89. alignItems: 'center',
  90. cursor:'pointer',
  91. width: 80,
  92. },
  93. bottomIcon: {
  94. cursor:'pointer',
  95. },
  96. bottomIconEndAccept: {
  97. cursor: 'pointer',
  98. '&:hover': {
  99. animation: `$shake 1s`,
  100. animationIterationCount:'infinite',
  101. }
  102. },
  103. ringPulsate: {
  104. backgroundColor:"rgb(80, 80, 80)",
  105. borderRadius: '50%',
  106. height: 60,
  107. width: 60,
  108. position: 'absolute',
  109. left: 10,
  110. top: -8,
  111. animation: `$pulsate 1.5s ease-out`,
  112. animationIterationCount: 'infinite',
  113. opacity: 0,
  114. },
  115. titleIconBottom: {
  116. color: '#ffffff',
  117. fontSize: 13,
  118. paddingTop:7
  119. },
  120. myVideo: {
  121. width: 250,
  122. height: 'auto',
  123. cursor: 'pointer',
  124. position: 'absolute',
  125. top: 0,
  126. left: 0,
  127. zIndex: 150,
  128. },
  129. '@keyframes pulsate': {
  130. '0%': {transform: 'scale(1, 1)', opacity: 0},
  131. '50%': { opacity: 1},
  132. '100%': {transform: 'scale(1.2, 1.2)', opacity: 0},
  133. },
  134. '@keyframes shake': {
  135. '0%': { transform: 'rotate(0deg)'},
  136. '11%': { transform: 'rotate(10deg)'},
  137. '22%': { transform: 'rotate(20deg)'},
  138. '33%': { transform: 'rotate(30deg)'},
  139. '44%': { transform: 'rotate(20deg)'},
  140. '55%': { transform: 'rotate(10deg)'},
  141. '66%': { transform: 'rotate(0deg)'},
  142. '77%': { transform: 'rotate(-10deg)'},
  143. '88%': { transform: 'rotate(-20deg)'},
  144. '100%': { transform: 'rotate(-30deg)'},
  145. },
  146. })
  147. interface ICallBar {
  148. callStatus:any,
  149. setCallStatus: any,
  150. }
  151. const CallBar = ({callStatus,setCallStatus}:ICallBar) => {
  152. const classes = useStyles()
  153. const { _id } = useSelector(getAuthorizationState)
  154. const chat = useSelector(getChat)
  155. const { socketId, companionId } = chat
  156. const connectionRef = useRef<any>(null);
  157. const myVideoRef = useRef<any>(null);
  158. const companionVideoRef = useRef<any>(null);
  159. const companionAudioRef = useRef<any>(null);
  160. const [mutedMyVideo,setMutedMyVideo] = useState<boolean>(false)
  161. const [mutedMyAudio,setMutedMyAudio] = useState<boolean>(false)
  162. const [mySocket, setMySocket] = useState<string>('')
  163. const [companionSocket, setCompanionSocket] = useState<string>('')
  164. const [myStream, setMyStream] = useState<any>(null)
  165. const [companionSignal, setCompanionSignal] = useState<string>('')
  166. const [name, setName] = useState<string>('')
  167. const [lastName, setLastName] = useState<string>('')
  168. const [avatarUrl, setAvatarUrl] = useState<string>('')
  169. const [color, setColor] = useState<string>('')
  170. const [number, setNumber] = useState<string>('')
  171. const [conversationLast, cetConversationLast] = useState<string>('')
  172. const [fullScreen, setFullScreen] = useState<boolean>(false)
  173. const handleConversationLast = (e: any) =>
  174. cetConversationLast(getTimeBySeconds(e.target.currentTime))
  175. const handleMuteVideo = () => {
  176. if (myStream&&myStream.getVideoTracks()[0]) {
  177. setMutedMyVideo(!mutedMyVideo)
  178. myStream.getVideoTracks()[0].enabled = !myStream.getVideoTracks()[0].enabled
  179. }
  180. }
  181. const handleMuteAudio = () => {
  182. if (myStream&&myStream.getAudioTracks()[0]) {
  183. setMutedMyAudio(!mutedMyAudio)
  184. myStream.getAudioTracks()[0].enabled = !myStream.getAudioTracks()[0].enabled
  185. }
  186. }
  187. const handleLeaveCall = () => {
  188. setCallStatus('hanging up...')
  189. connectionRef.current.destroy();
  190. setCallStatus('')
  191. };
  192. const handleStartCall = useCallback(async () => {
  193. setCallStatus('waiting...')
  194. const mediaDevices: any = navigator.mediaDevices
  195. const stream = await mediaDevices.getDisplayMedia({
  196. video: true,
  197. audio: true
  198. })
  199. setMyStream(stream)
  200. myVideoRef.current.srcObject = stream;
  201. const peer = new Peer({
  202. initiator: true,
  203. trickle: false,
  204. stream
  205. });
  206. setCallStatus('ringing...')
  207. peer.on("signal", (data: any) => {
  208. socket.emit("callTo", {
  209. to: socketId,
  210. signalData: data,
  211. from: mySocket,
  212. userId: _id,
  213. companionId
  214. })
  215. });
  216. peer.on("stream", (companionStream: any) => {
  217. companionVideoRef.current.srcObject = companionStream;
  218. companionAudioRef.current.srcObject = companionStream;
  219. });
  220. peer.on('error', (e: any) => console.log('error from peer', e))
  221. peer.on('connect', () => {
  222. console.log('CONNECT')
  223. })
  224. socket.on("acceptedCall", ({ signal }: any) => {
  225. setCallStatus('connection')
  226. peer.signal(signal)
  227. setCallStatus('conversation')
  228. });
  229. connectionRef.current = peer;
  230. },[socketId,companionId,_id,mySocket,myVideoRef,setCallStatus])
  231. const handleAnswerCall = useCallback(async () => {
  232. setCallStatus('connection')
  233. const stream = await navigator.mediaDevices.getUserMedia({
  234. video: true,
  235. audio: true
  236. })
  237. setMyStream(stream)
  238. myVideoRef.current.srcObject = stream;
  239. const peer = new Peer({
  240. initiator: false,
  241. trickle: false,
  242. stream,
  243. });
  244. peer.on("signal", (data: any) => {
  245. socket.emit("answerCall", {
  246. signal: data,
  247. to: companionSocket,
  248. });
  249. setCallStatus('conversation')
  250. });
  251. peer.on("stream", (companionStream: any) => {
  252. companionVideoRef.current.srcObject = companionStream;
  253. companionAudioRef.current.srcObject = companionStream;
  254. });
  255. peer.signal(companionSignal);
  256. connectionRef.current = peer;
  257. },[companionSocket,companionSignal,setCallStatus])
  258. useEffect(() => {
  259. socket.on("me", (id: string) => {
  260. setMySocket(id)
  261. socketIdChat(id)
  262. })
  263. socket.on('incomeCall', ({ name, lastName, avatarUrl, color, number, from, signal }: any) => {
  264. if (connectionRef.current === null) {
  265. setCallStatus('is calling you')
  266. setName(name)
  267. setLastName(lastName)
  268. setAvatarUrl(avatarUrl)
  269. setColor(color)
  270. setNumber(number)
  271. setCompanionSocket(from)
  272. setCompanionSignal(signal)
  273. }
  274. })
  275. }, [setCallStatus])
  276. useEffect(() => {
  277. if(callStatus === 'requesting...') handleStartCall()
  278. }, [callStatus, handleStartCall])
  279. useEffect(() => {
  280. if (callStatus === '') {
  281. setName(chat.name)
  282. setLastName(chat.lastName)
  283. setAvatarUrl(chat.avatarUrl)
  284. setColor(chat.color)
  285. setNumber(chat.number)
  286. }
  287. }, [callStatus, chat])
  288. return (
  289. <div className={classes.container} style={{ top: callStatus ? 0 : '-100%'}}>
  290. <video className={classes.myVideo} ref={myVideoRef} playsInline autoPlay muted controls={false}/>
  291. <Moveable
  292. target={myVideoRef.current}
  293. draggable={true}
  294. throttleDrag={0}
  295. hideDefaultLines={true}
  296. renderDirections={[]}
  297. rotationPosition="none"
  298. origin={false}
  299. onDrag={({ target, transform }: OnDrag) =>
  300. target!.style.transform = transform }
  301. />
  302. <div className={classes.modalCall} style={{width: fullScreen?'100vw':'34vw',height:fullScreen?'100vh':'auto'}}>
  303. <div className={classes.rightIcons}>
  304. <div className={classes.rightIconWrapper} onClick={() => setFullScreen(false)}
  305. style={{backgroundColor:fullScreen?'transparent':'rgb(70, 70, 70)'}}>
  306. <MinimizeIcon fontSize='small' />
  307. </div>
  308. <div className={classes.rightIconWrapper} onClick={() => setFullScreen(true)}
  309. style={{backgroundColor:fullScreen?'rgb(70, 70, 70)':'transparent'}}>
  310. <CropLandscapeIcon fontSize='small' />
  311. </div>
  312. <div className={classes.rightIconWrapperClose} onClick={handleLeaveCall}>
  313. <CloseIcon fontSize='small' />
  314. </div>
  315. </div>
  316. {callStatus !== 'conversation'&&<ListItemAvatar style={{margin:'25px 0px 5px 0px'}}>
  317. <Avatar alt={name} src={avatarUrl?`${prodAwsS3}/${avatarUrl}`:undefined}
  318. sx={{ background: color, width: 120, height: 120, marginRight: 2, fontSize:30,zIndex:0}}>
  319. {`${firstLetter(name)}${firstLetter(lastName)}`}
  320. </Avatar>
  321. </ListItemAvatar>}
  322. {/* <ListItemText primary={`${firstLetter(name)}${slicedWord(name, 15, 1)}
  323. ${firstLetter(lastName)}${slicedWord(lastName, 15, 1)}`}
  324. primaryTypographyProps={{ color: '#dfdfdf', fontSize: 20, fontWeight: 500 }}/>
  325. <ListItemText primary={number} primaryTypographyProps={{ color: '#ffffff', fontSize: 15, fontWeight: 500, textAlign: "center" }} />
  326. <ListItemText secondary={callStatus} secondaryTypographyProps={{ color: "#dfdfdf", textAlign: "center" }} />
  327. <ListItemText secondary={conversationLast} secondaryTypographyProps={{ color: "#dfdfdf", textAlign: "center" }} /> */}
  328. <video ref={companionVideoRef} playsInline muted autoPlay controls={false}
  329. style={{width: '100%',height:fullScreen?'100vh': 'auto',objectFit: 'cover'}} onTimeUpdate={handleConversationLast} />
  330. <audio ref={companionAudioRef} autoPlay />
  331. <div className={classes.bottomWrapper}>
  332. <div className={classes.bottomItem} onClick={handleMuteVideo}>
  333. <Avatar className={classes.bottomIcon}
  334. sx={{backgroundColor: mutedMyVideo?'#ffffff':'rgb(88, 88, 88)',color: mutedMyVideo?'rgb(36, 36, 36)':'#ffffff', width: 44, height: 44,zIndex:0}}>
  335. {mutedMyVideo?<VideocamOffIcon fontSize="medium" />:<VideocamIcon fontSize="medium" />}
  336. </Avatar>
  337. <Typography variant="h6" className={classes.titleIconBottom}>{mutedMyVideo?'Start Video':'Stop Video'}</Typography>
  338. </div>
  339. <div className={classes.bottomItem}>
  340. <Avatar className={classes.bottomIconEndAccept} onClick={handleLeaveCall}
  341. sx={{backgroundColor: '#f02a2a',color: '#ffffff', width: 44, height: 44,zIndex:0}}>
  342. <CallEndIcon fontSize="medium" />
  343. </Avatar>
  344. <Typography variant="h6" className={classes.titleIconBottom}>
  345. {callStatus === 'is calling you' ? 'Decline' : 'End Call'}
  346. </Typography>
  347. </div>
  348. {callStatus === 'is calling you' &&
  349. <div className={classes.bottomItem} style={{position:"relative"}}>
  350. <div className={classes.ringPulsate}></div>
  351. <Avatar className={classes.bottomIconEndAccept} onClick={handleAnswerCall}
  352. sx={{ backgroundColor: '#21f519', color: '#ffffff', width: 44, height: 44, zIndex: 0 }}>
  353. <PhoneIcon fontSize="medium" />
  354. </Avatar>
  355. <Typography variant="h6" className={classes.titleIconBottom}>
  356. Accept
  357. </Typography>
  358. </div>}
  359. <div className={classes.bottomItem}>
  360. <Avatar className={classes.bottomIcon} onClick={handleMuteAudio}
  361. sx={{backgroundColor: mutedMyAudio?'#ffffff':'rgb(88, 88, 88)',color: mutedMyAudio?'rgb(36, 36, 36)':'#ffffff', width: 44, height: 44,zIndex:0}}>
  362. {mutedMyAudio?<MicOffIcon fontSize="medium" />:<MicIcon fontSize="medium" />}
  363. </Avatar>
  364. <Typography variant="h6" className={classes.titleIconBottom}>{mutedMyAudio?'Unmute':'Mute'}</Typography>
  365. </div>
  366. </div>
  367. </div>
  368. </div>
  369. )
  370. }
  371. export default CallBar