index.tsx 19 KB

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