index.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  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 Alert from '@mui/material/Alert';
  20. import { getChat } from '../../../redux/chat/selector';
  21. import { getAuthorizationState } from '../../../redux/authorization/selector';
  22. import { prodAwsS3,prodBaseURL,prodSocketURL, firstLetter, slicedWord,getTimeBySeconds,playNotification } from '../../../helpers'
  23. import { socketIdChat } from '../../../api-data';
  24. const Peer = require('simple-peer')
  25. const socket = io(prodSocketURL)
  26. const useStyles = makeStyles({
  27. container: {
  28. position: 'absolute',
  29. left: 0,
  30. top: 0,
  31. width: '100vw',
  32. height:'100vh',
  33. overflow: 'hidden',
  34. zIndex:100,
  35. display: 'flex',
  36. justifyContent: 'center',
  37. alignItems: 'center',
  38. alignContent: "center",
  39. backgroundColor: 'rgba(104, 105, 104, 0.6)',
  40. },
  41. modalCall: {
  42. background: 'rgb(36, 36, 36)',
  43. position:'relative',
  44. display: 'flex',
  45. flexDirection: 'column',
  46. justifyContent: 'start',
  47. alignItems: 'center',
  48. justifyItems:"center",
  49. borderRadius: 7,
  50. },
  51. rightIcons: {
  52. display: 'flex',
  53. justifyContent: 'end',
  54. alignContent: 'center',
  55. alignItems: 'center',
  56. width: '100%',
  57. position: 'absolute',
  58. left: 0,
  59. top: 0,
  60. zIndex:1
  61. },
  62. rightIconWrapper: {
  63. color: '#ffffff',
  64. cursor: 'pointer',
  65. padding:'3px 10px 3px 10px',
  66. },
  67. rightIconWrapperClose: {
  68. color: '#ffffff',
  69. cursor: 'pointer',
  70. padding: '3px 10px 3px 10px',
  71. backgroundColor:'rgb(36, 36, 36)',
  72. borderTopRightRadius:7,
  73. '&:hover': {
  74. backgroundColor:'#f02a2a'
  75. }
  76. },
  77. bottomWrapper: {
  78. display: 'flex',
  79. justifyContent: 'center',
  80. padding: 5,
  81. position: "absolute",
  82. left: 0,
  83. bottom: 0,
  84. width:'100%'
  85. },
  86. bottomItem: {
  87. display: 'flex',
  88. flexDirection: 'column',
  89. justifyContent: 'center',
  90. alignContent: 'center',
  91. alignItems: 'center',
  92. cursor:'pointer',
  93. width: 80,
  94. },
  95. bottomIcon: {
  96. cursor:'pointer',
  97. },
  98. bottomIconEndAccept: {
  99. cursor: 'pointer',
  100. '&:hover': {
  101. animation: `$shake 1s`,
  102. animationIterationCount:'infinite',
  103. }
  104. },
  105. ringPulsate: {
  106. backgroundColor:"rgb(80, 80, 80)",
  107. borderRadius: '50%',
  108. height: 60,
  109. width: 60,
  110. position: 'absolute',
  111. left: 10,
  112. top: -8,
  113. animation: `$pulsate 1.5s ease-out`,
  114. animationIterationCount: 'infinite',
  115. opacity: 0,
  116. },
  117. titleIconBottom: {
  118. color: '#ffffff',
  119. fontSize: 13,
  120. paddingTop:7
  121. },
  122. myVideo: {
  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 idAudioIntervalRef = useRef<any>(null);
  158. const myVideoRef = useRef<any>(null);
  159. const companionVideoRef = useRef<any>(null);
  160. const companionAudioRef = useRef<any>(null);
  161. const [mySocket, setMySocket] = useState<string>('')
  162. const [mutedMyVideo,setMutedMyVideo] = useState<boolean>(false)
  163. const [mutedMyAudio,setMutedMyAudio] = useState<boolean>(false)
  164. const [companionSocket, setCompanionSocket] = useState<string>('')
  165. const [myStream, setMyStream] = useState<any>(null)
  166. const [companionSignal, setCompanionSignal] = useState<any>(null)
  167. const [name, setName] = useState<string>('')
  168. const [lastName, setLastName] = useState<string>('')
  169. const [avatarUrl, setAvatarUrl] = useState<string>('')
  170. const [color, setColor] = useState<string>('')
  171. const [number, setNumber] = useState<string>('')
  172. const [callLast, setCallLast] = useState<number>(0)
  173. const [conversationLast, cetConversationLast] = useState<string>('')
  174. const [fullScreen, setFullScreen] = useState<boolean>(false)
  175. const [alert, setAlert] = useState<string>('')
  176. const [audioHtml, setAudioHtml] = useState<any>(null)
  177. const handleLeaveCall = useCallback(() => {
  178. if (callStatus === 'is calling you') {
  179. socket.emit("answerCall", {
  180. signal: 'declined',
  181. to: companionSocket,
  182. });
  183. }
  184. if(connectionRef.current) connectionRef.current.destroy();
  185. if(idAudioIntervalRef.current) clearInterval(idAudioIntervalRef.current)
  186. myVideoRef.current.srcObject = null
  187. companionVideoRef.current.srcObject = null
  188. companionAudioRef.current.srcObject = null
  189. if (audioHtml) {
  190. audioHtml.pause()
  191. setAudioHtml(null)
  192. }
  193. if(myStream) myStream.getTracks().forEach((track:any) => track.stop())
  194. setMutedMyVideo(false)
  195. setMutedMyAudio(false)
  196. setCompanionSocket('')
  197. setMyStream(null)
  198. setCompanionSignal(null)
  199. setName('')
  200. setLastName('')
  201. setAvatarUrl('')
  202. setColor('')
  203. setNumber('')
  204. setCallLast(0)
  205. cetConversationLast('')
  206. setFullScreen(false)
  207. setAlert('')
  208. setCallStatus('')
  209. }, [setCallStatus, callStatus, companionSocket, audioHtml, myStream])
  210. const handleHangUp = () =>
  211. setCallStatus('hanging up...')
  212. const handleConversationLast = (e: any) =>
  213. cetConversationLast(getTimeBySeconds(e.target.currentTime))
  214. const handleMuteVideo = () => {
  215. if (myStream&&myStream.getVideoTracks()[0]) {
  216. setMutedMyVideo(!mutedMyVideo)
  217. myStream.getVideoTracks()[0].enabled = !myStream.getVideoTracks()[0].enabled
  218. } else {
  219. setAlert(`You can not ${mutedMyVideo?'enable':'disable'} Video before stream started`)
  220. }
  221. }
  222. const handleMuteAudio = () => {
  223. if (myStream&&myStream.getAudioTracks()[0]) {
  224. setMutedMyAudio(!mutedMyAudio)
  225. myStream.getAudioTracks()[0].enabled = !myStream.getAudioTracks()[0].enabled
  226. } else {
  227. setAlert(`You can not ${mutedMyAudio?'enable':'disable'} Audio before stream started`)
  228. }
  229. }
  230. const handleStartCall = useCallback(async () => {
  231. try {
  232. setCallStatus('waiting...')
  233. const mediaDevices: any = navigator.mediaDevices
  234. const stream = await mediaDevices.getDisplayMedia({
  235. video: true,
  236. audio: true
  237. })
  238. setMyStream(stream)
  239. myVideoRef.current.srcObject = stream;
  240. const peer = new Peer({
  241. initiator: true,
  242. trickle: false,
  243. stream
  244. });
  245. const audioRing = playNotification(`${prodBaseURL}/calling.mp3`)
  246. audioRing.loop = true
  247. setAudioHtml(audioRing)
  248. idAudioIntervalRef.current = setInterval(() =>
  249. setCallLast(prevState => prevState + 1), 1000)
  250. peer.on("signal", (data: any) => {
  251. setCallStatus('ringing...')
  252. socket.emit("callTo", {
  253. to: socketId,
  254. signalData: data,
  255. from: mySocket,
  256. userId: _id,
  257. companionId,
  258. peer
  259. })
  260. });
  261. peer.on("stream", (companionStream: any) => {
  262. companionVideoRef.current.srcObject = companionStream;
  263. companionAudioRef.current.srcObject = companionStream;
  264. });
  265. peer.on('connect', () => {
  266. setCallStatus('connection')
  267. setAlert('')
  268. audioRing.pause()
  269. clearInterval(idAudioIntervalRef.current)
  270. setTimeout(() => setCallStatus('conversation'),1000)
  271. })
  272. peer.on("close", () => setCallStatus('hanging up...'));
  273. peer.on('error', () => setCallStatus('connection lost'))
  274. socket.on("acceptedCall", ({ signal }: any) => {
  275. if (signal === 'declined') setCallStatus('request declined')
  276. else if (signal === 'busy') setCallStatus('line busy')
  277. else peer.signal(signal)
  278. });
  279. connectionRef.current = peer;
  280. } catch (e:any) {
  281. setCallStatus('permission not allowed')
  282. }
  283. },[socketId,companionId,_id,mySocket,myVideoRef,setCallStatus])
  284. const handleAnswerCall = useCallback(async () => {
  285. try {
  286. setCallStatus('waiting...')
  287. audioHtml.pause()
  288. const stream = await navigator.mediaDevices.getUserMedia({
  289. video: true,
  290. audio: true
  291. })
  292. setMyStream(stream)
  293. myVideoRef.current.srcObject = stream;
  294. const peer = new Peer({
  295. initiator: false,
  296. trickle: false,
  297. stream,
  298. });
  299. peer.on("signal", (data: any) => {
  300. socket.emit("answerCall", {
  301. signal: data,
  302. to: companionSocket,
  303. });
  304. });
  305. peer.on("stream", (companionStream: any) => {
  306. companionVideoRef.current.srcObject = companionStream;
  307. companionAudioRef.current.srcObject = companionStream;
  308. });
  309. peer.on('connect', () => {
  310. setCallStatus('connection')
  311. setAlert('')
  312. setTimeout(() => setCallStatus('conversation'),1000)
  313. })
  314. peer.on("close", () => setCallStatus('hanging up...'));
  315. peer.on('error', () => setCallStatus('connection lost'))
  316. peer.signal(companionSignal);
  317. connectionRef.current = peer;
  318. } catch (e: any) {
  319. setCallStatus('permission not allowed')
  320. }
  321. }, [companionSocket, companionSignal, setCallStatus,audioHtml])
  322. useEffect(() => {
  323. socket.on("me", (id: string) => {
  324. setMySocket(id)
  325. socketIdChat(id)
  326. })
  327. },[])
  328. useEffect(() => {
  329. socket.on('incomeCall', ({ name, lastName, avatarUrl, color, number, from, signal }: any) => {
  330. if (connectionRef.current === null) {
  331. setCallStatus('is calling you')
  332. setName(name)
  333. setLastName(lastName)
  334. setAvatarUrl(avatarUrl)
  335. setColor(color)
  336. setNumber(number)
  337. setCompanionSocket(from)
  338. setCompanionSignal(signal)
  339. const audioRing = playNotification(`${prodBaseURL}/ringing.mp3`)
  340. audioRing.loop = true
  341. setAudioHtml(audioRing)
  342. } else if (companionSocket !== from) {
  343. socket.emit("answerCall", {
  344. signal: 'busy',
  345. to: companionSocket,
  346. });
  347. }
  348. })
  349. }, [setCallStatus,companionSocket])
  350. useEffect(() => {
  351. if (callLast === 60) setCallStatus('have not got response')
  352. }, [callLast, setCallStatus])
  353. useEffect(() => {
  354. if(callStatus === 'requesting...') handleStartCall()
  355. }, [callStatus, handleStartCall])
  356. useEffect(() => {
  357. if (callStatus === 'hanging up...' || callStatus === 'connection lost'
  358. || callStatus === 'request declined' || callStatus === 'have not got response'
  359. || callStatus === 'permission not allowed' || callStatus === 'line busy')
  360. handleLeaveCall()
  361. }, [callStatus, handleLeaveCall, setCallStatus])
  362. useEffect(() => {
  363. if (callStatus === '') {
  364. setName(chat.name)
  365. setLastName(chat.lastName)
  366. setAvatarUrl(chat.avatarUrl)
  367. setColor(chat.color)
  368. setNumber(chat.number)
  369. }
  370. }, [callStatus, chat])
  371. return (
  372. <div className={classes.container} style={{ top: callStatus ? 0 : '-100%'}}>
  373. <video className={classes.myVideo} style={{width: !myStream || mutedMyVideo?0:250,height: !myStream || mutedMyVideo?0:'auto'}}
  374. ref={myVideoRef} playsInline autoPlay muted controls={false} />
  375. <Moveable
  376. target={myVideoRef.current}
  377. draggable={true}
  378. throttleDrag={0}
  379. hideDefaultLines={true}
  380. renderDirections={[]}
  381. rotationPosition="none"
  382. origin={false}
  383. onDrag={({ target, transform }: OnDrag) =>
  384. target!.style.transform = transform }
  385. />
  386. <div className={classes.modalCall} style={{width: fullScreen?'100vw':'34vw',height:fullScreen?'100vh':'auto'}}>
  387. <div className={classes.rightIcons}>
  388. <div className={classes.rightIconWrapper} onClick={() => setFullScreen(false)}
  389. style={{backgroundColor:fullScreen?'rgb(36, 36, 36)':'rgb(70, 70, 70)'}}>
  390. <MinimizeIcon fontSize='small' />
  391. </div>
  392. <div className={classes.rightIconWrapper} onClick={() => setFullScreen(true)}
  393. style={{backgroundColor:fullScreen?'rgb(70, 70, 70)':'rgb(36, 36, 36)'}}>
  394. <CropLandscapeIcon fontSize='small' />
  395. </div>
  396. <div className={classes.rightIconWrapperClose} onClick={handleHangUp}>
  397. <CloseIcon fontSize='small' />
  398. </div>
  399. </div>
  400. <div style={{ width: '100%', position: "relative" }}>
  401. {alert && <Alert variant="filled" severity="info" sx={{ backgroundColor: "rgb(70, 70, 70)" }}
  402. style={{ width: fullScreen?'auto':"100%", position: 'absolute', left: 0, bottom: -48 }}>{alert}</Alert>}
  403. {!fullScreen && <>
  404. <ListItemAvatar style={{margin:'25px 0px 5px 0px'}}>
  405. <Avatar alt={name} src={avatarUrl?`${prodAwsS3}/${avatarUrl}`:undefined}
  406. sx={{ background: color, width: 120, height: 120, marginRight: 2, fontSize:30,zIndex:0,margin:'0 auto'}}>
  407. {`${firstLetter(name)}${firstLetter(lastName)}`}
  408. </Avatar>
  409. </ListItemAvatar>
  410. <ListItemText primary={`${firstLetter(name)}${slicedWord(name, 15, 1)}
  411. ${firstLetter(lastName)}${slicedWord(lastName, 15, 1)}`}
  412. primaryTypographyProps={{ color: '#dfdfdf', fontSize: 20, fontWeight: 500,textAlign: "center" }}/>
  413. <ListItemText primary={number} primaryTypographyProps={{ color: '#ffffff', fontSize: 15, fontWeight: 500, textAlign: "center" }} />
  414. <ListItemText secondary={callStatus} secondaryTypographyProps={{ color: "#dfdfdf", textAlign: "center" }} />
  415. <ListItemText secondary={conversationLast} secondaryTypographyProps={{ color: "#dfdfdf", textAlign: "center" }} />
  416. </>}
  417. </div>
  418. <video ref={companionVideoRef} playsInline muted autoPlay controls={false}
  419. style={{width: '100%',height:fullScreen?'100vh': 'auto',objectFit: 'cover',
  420. backgroundColor:'rgb(36, 36, 36)'}} onTimeUpdate={handleConversationLast} />
  421. <audio ref={companionAudioRef} autoPlay />
  422. <div className={classes.bottomWrapper}>
  423. <div className={classes.bottomItem} onClick={handleMuteVideo}>
  424. <Avatar className={classes.bottomIcon}
  425. sx={{backgroundColor: mutedMyVideo?'#ffffff':'rgb(88, 88, 88)',color: mutedMyVideo?'rgb(36, 36, 36)':'#ffffff', width: 44, height: 44,zIndex:0}}>
  426. {mutedMyVideo?<VideocamOffIcon fontSize="medium" />:<VideocamIcon fontSize="medium" />}
  427. </Avatar>
  428. <Typography variant="h6" className={classes.titleIconBottom}>{mutedMyVideo?'Start Video':'Stop Video'}</Typography>
  429. </div>
  430. <div className={classes.bottomItem}>
  431. <Avatar className={classes.bottomIconEndAccept} onClick={handleHangUp}
  432. sx={{backgroundColor: '#f02a2a',color: '#ffffff', width: 44, height: 44,zIndex:0}}>
  433. <CallEndIcon fontSize="medium" />
  434. </Avatar>
  435. <Typography variant="h6" className={classes.titleIconBottom}>
  436. {callStatus === 'is calling you' ? 'Decline' : 'End Call'}
  437. </Typography>
  438. </div>
  439. {callStatus === 'is calling you' &&
  440. <div className={classes.bottomItem} style={{position:"relative"}}>
  441. <div className={classes.ringPulsate}></div>
  442. <Avatar className={classes.bottomIconEndAccept} onClick={handleAnswerCall}
  443. sx={{ backgroundColor: '#21f519', color: '#ffffff', width: 44, height: 44, zIndex: 0 }}>
  444. <PhoneIcon fontSize="medium" />
  445. </Avatar>
  446. <Typography variant="h6" className={classes.titleIconBottom}>
  447. Accept
  448. </Typography>
  449. </div>}
  450. <div className={classes.bottomItem}>
  451. <Avatar className={classes.bottomIcon} onClick={handleMuteAudio}
  452. sx={{backgroundColor: mutedMyAudio?'#ffffff':'rgb(88, 88, 88)',color: mutedMyAudio?'rgb(36, 36, 36)':'#ffffff', width: 44, height: 44,zIndex:0}}>
  453. {mutedMyAudio?<MicOffIcon fontSize="medium" />:<MicIcon fontSize="medium" />}
  454. </Avatar>
  455. <Typography variant="h6" className={classes.titleIconBottom}>{mutedMyAudio?'Unmute':'Mute'}</Typography>
  456. </div>
  457. </div>
  458. </div>
  459. </div>
  460. )
  461. }
  462. export default CallBar