index.tsx 19 KB

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