Last active
February 9, 2021 11:02
-
-
Save AdamZaczek/acccddc7158351e4f8abcad2d394fe03 to your computer and use it in GitHub Desktop.
It works I guess
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useRef, PureComponent } from 'react' | |
import AgoraRTC from 'agora-rtc-sdk' | |
import styled from 'styled-components/macro' | |
import { VideoCameraOutlined } from '@ant-design/icons' | |
import { AudioOutlined } from '@ant-design/icons' | |
import { PhoneOutlined } from '@ant-design/icons' | |
import SmallSpinner from 'components/smallSpinner' | |
import CircularProgress from '@material-ui/core/CircularProgress' | |
const StyledVideo = styled.video` | |
border-radius: 5px; | |
width: 150px; | |
border: 1px solid ${({ theme }) => theme.colors.c1}; | |
${({ hidden }) => | |
hidden && | |
` | |
visibility: hidden; | |
`}; | |
` | |
const NoVideo = styled.div` | |
border-radius: 5px; | |
width: 150px; | |
height: 115px; | |
border: 1px solid ${({ theme }) => theme.colors.c1}; | |
background: ${({ theme }) => theme.colors.c3}; | |
color: ${({ theme }) => theme.colors.primaryColor}; | |
display: grid; | |
place-items: center; | |
` | |
const IconsWrapper = styled.div` | |
position: relative; | |
bottom: 40px; | |
display: flex; | |
z-index: 2; | |
color: ${({ theme }) => theme.colors.c1}; | |
justify-content: space-around; | |
` | |
const StyledCameraIcon = styled(VideoCameraOutlined)` | |
font-size: 30px; | |
transition: color 0.3s; | |
cursor: pointer; | |
color: ${({ theme, active }) => | |
active ? theme.colors.c1 : theme.colors.c23}; | |
:hover { | |
color: ${({ theme }) => theme.colors.c2} !important; | |
} | |
` | |
const StyledAudioIcon = styled(AudioOutlined)` | |
font-size: 30px; | |
transition: color 0.3s; | |
cursor: pointer; | |
color: ${({ theme, active }) => | |
active ? theme.colors.c1 : theme.colors.c23}; | |
:hover { | |
color: ${({ theme }) => theme.colors.c2} !important; | |
} | |
` | |
const StyledPhoneIcon = styled(PhoneOutlined)` | |
font-size: 30px; | |
transition: color 0.3s; | |
cursor: pointer; | |
color: ${({ theme, active }) => theme.colors.c23}; | |
:hover { | |
color: ${({ theme }) => theme.colors.c2} !important; | |
} | |
` | |
const Video = ({ srcObject, ...props }) => { | |
const refVideo = useRef() | |
useEffect(() => { | |
if (!refVideo.current) return | |
refVideo.current.srcObject = srcObject | |
}, [srcObject]) | |
return <StyledVideo ref={refVideo} {...props} /> | |
} | |
const VideoWrapper = styled.div` | |
display: flex; | |
flex-direction: row-reverse; | |
position: relative; | |
` | |
const OuterVideoWrapper = styled(VideoWrapper)` | |
flex-direction: column; | |
width: 150px; | |
` | |
const OuterSpinnerWrapper = styled.div` | |
border-radius: 5px; | |
padding: 10px; | |
background: ${({ theme }) => theme.colors.primaryBackground}; | |
` | |
const SpinnerWrapper = styled.div` | |
border-radius: 5px; | |
min-width: 50px; | |
min-height: 50px; | |
padding: 10px; | |
position: relative; | |
background: ${({ theme }) => theme.colors.primaryBackground}; | |
` | |
const StyledInfo = styled.div` | |
color: ${({ theme }) => theme.colors.secondaryColor}; | |
background: ${({ theme }) => theme.colors.primaryBackground}; | |
font-size: 16px; | |
padding: 5px; | |
text-overflow: ellipsis; | |
border-radius: 5px; | |
flex: 0 0 auto; | |
width: 150px; | |
height: 115px; | |
font-weight: 500; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
` | |
const VideoItemWrapper = styled.div` | |
width: 150px; | |
flex: 0 0 auto; | |
` | |
class VideoContainer extends PureComponent { | |
constructor(props) { | |
super(props) | |
this.client = {} | |
this.localStream = {} | |
this.state = { | |
displayMode: 'pip', | |
streamList: [], | |
readyState: false, | |
isVideoOn: true, | |
isAudioOn: true, | |
} | |
} | |
componentWillMount() { | |
const { | |
appId, | |
channel, | |
uid, | |
attendeeMode, | |
videoProfile, | |
transcode, | |
userName, | |
} = this.props | |
// init AgoraRTC local client | |
this.client = AgoraRTC.createClient({ mode: transcode }) | |
this.client.init(appId, () => { | |
/* For some reason those console logs are not visible in devtools but the logic is needed. */ | |
this.subscribeStreamEvents() | |
this.client.join(appId, channel, uid, async uid => { | |
// console.log('User ' + uid + ' join channel successfully') | |
// console.log('At ' + new Date().toLocaleTimeString()) | |
// create local stream | |
// It is not recommended to setState in function addStream | |
this.localStream = this.streamInit(uid, attendeeMode, videoProfile, { | |
userName, | |
video: await this.detectWebcam(), | |
}) | |
this.localStream.init( | |
() => { | |
if (attendeeMode !== 'audience') { | |
this.addStream(this.localStream, true) | |
this.client.publish(this.localStream, err => { | |
console.log('Publish local stream error: ' + err) | |
}) | |
} | |
this.setState({ readyState: true }) | |
}, | |
err => { | |
console.log('getUserMedia failed', err) | |
this.setState({ readyState: true }) | |
} | |
) | |
}) | |
}) | |
} | |
detectWebcam = async () => { | |
let md = navigator.mediaDevices | |
if (!md || !md.enumerateDevices) return false | |
const devices = await md.enumerateDevices() | |
return devices.some(device => 'videoinput' === device.kind) | |
} | |
componentWillUnmount() { | |
this.client && this.client.unpublish(this.localStream) | |
this.localStream && this.localStream.close() | |
this.client && | |
this.client.leave( | |
() => { | |
console.log('Client succeed to leave.') | |
}, | |
() => { | |
console.log('Client failed to leave.') | |
} | |
) | |
} | |
streamInit = (uid, attendeeMode, videoProfile, config) => { | |
let defaultConfig = { | |
streamID: uid, | |
audio: true, | |
video: true, | |
screen: false, | |
AEC: true, | |
...config, | |
} | |
switch (attendeeMode) { | |
case 'audio-only': | |
defaultConfig.video = false | |
break | |
case 'audience': | |
defaultConfig.video = false | |
defaultConfig.audio = false | |
break | |
default: | |
case 'video': | |
break | |
} | |
let stream = AgoraRTC.createStream({ ...defaultConfig, ...config }) | |
stream.setVideoProfile(videoProfile) | |
return stream | |
} | |
subscribeStreamEvents = () => { | |
let rt = this | |
rt.client.on('stream-added', function(evt) { | |
let stream = evt.stream | |
// console.log('New stream added: ' + stream.getId()) | |
// console.log('At ' + new Date().toLocaleTimeString()) | |
// console.log('Subscribe ', stream) | |
rt.client.subscribe(stream, function(err) { | |
console.log('Subscribe stream failed', err) | |
}) | |
}) | |
rt.client.on('peer-leave', function(evt) { | |
// console.log('Peer has left: ' + evt.uid) | |
// console.log(new Date().toLocaleTimeString()) | |
// console.log(evt) | |
rt.removeStream(evt.uid) | |
}) | |
rt.client.on('stream-subscribed', function(evt) { | |
let stream = evt.stream | |
// console.log('Got stream-subscribed event') | |
// console.log(new Date().toLocaleTimeString()) | |
// console.log('Subscribe remote stream successfully: ' + stream.getId()) | |
// console.log(evt) | |
rt.addStream(stream) | |
}) | |
rt.client.on('stream-removed', function(evt) { | |
let stream = evt.stream | |
// console.log('Stream removed: ' + stream.getId()) | |
// console.log(new Date().toLocaleTimeString()) | |
// console.log(evt) | |
rt.removeStream(stream.getId()) | |
}) | |
} | |
removeStream = uid => { | |
const { streamList } = this.state | |
streamList.forEach((item, index) => { | |
if (item.getId() === uid) { | |
item.close() | |
let element = document.querySelector('#ag-item-' + uid) | |
if (element) { | |
element.parentNode.removeChild(element) | |
} | |
let tempList = [...streamList] | |
tempList.splice(index, 1) | |
this.setState({ | |
streamList: tempList, | |
}) | |
} | |
}) | |
} | |
addStream = (stream, push = false) => { | |
const { streamList } = this.state | |
let repetition = streamList.some(item => { | |
return item.getId() === stream.getId() | |
}) | |
if (repetition) { | |
return | |
} | |
if (push) { | |
this.setState({ | |
streamList: this.state.streamList.concat([stream]), | |
}) | |
} else { | |
this.setState({ | |
streamList: [stream].concat(this.state.streamList), | |
}) | |
} | |
} | |
handleCamera = e => { | |
e.currentTarget.classList.toggle('off') | |
this.localStream.isVideoOn() | |
? this.localStream.disableVideo() | |
: this.localStream.enableVideo() | |
this.setState({ isVideoOn: this.localStream.isVideoOn() }) | |
} | |
handleMic = e => { | |
e.currentTarget.classList.toggle('off') | |
this.localStream.isAudioOn() | |
? this.localStream.disableAudio() | |
: this.localStream.enableAudio() | |
this.setState({ isAudioOn: this.localStream.isAudioOn() }) | |
} | |
handleExit = e => { | |
if (e.currentTarget.classList.contains('disabled')) { | |
return | |
} | |
try { | |
this.client && this.client.unpublish(this.localStream) | |
this.localStream && this.localStream.close() | |
this.client && | |
this.client.leave( | |
() => { | |
console.log('Client succeed to leave.') | |
}, | |
() => { | |
console.log('Client failed to leave.') | |
} | |
) | |
} finally { | |
this.setState({ readyState: false }) | |
this.client = null | |
this.localStream = null | |
} | |
} | |
render() { | |
const { streamList, readyState, isAudioOn, isVideoOn } = this.state | |
const { uid, setVideoOn } = this.props | |
if (!streamList?.length) { | |
return ( | |
<StyledInfo> | |
Waiting for others to toggle video | |
<CircularProgress size="30px" thickness={5} /> | |
</StyledInfo> | |
) | |
} | |
return ( | |
<OuterVideoWrapper> | |
<VideoWrapper id="videoCall"> | |
{streamList?.length === 1 && ( | |
<StyledInfo> | |
Waiting for others to toggle video | |
<CircularProgress size="30px" thickness={5} /> | |
</StyledInfo> | |
)} | |
{readyState ? ( | |
<> | |
{streamList.map(stream => ( | |
<VideoItemWrapper> | |
<Video | |
muted={stream?.params?.streamID === uid} | |
id={stream?.stream?.id} | |
autoPlay | |
srcObject={stream?.stream} | |
hidden={!stream?.video || !stream?.audio} | |
/> | |
{!stream?.video ? ( | |
<NoVideo>No video</NoVideo> | |
) : ( | |
!stream?.audio && <NoVideo>No audio</NoVideo> | |
)} | |
{stream?.params?.streamID === uid && ( | |
<IconsWrapper> | |
{stream?.audio && ( | |
<StyledAudioIcon | |
onClick={this.handleMic} | |
active={isAudioOn} | |
/> | |
)} | |
<StyledPhoneIcon onClick={() => setVideoOn(false)} /> | |
{stream?.video && ( | |
<StyledCameraIcon | |
onClick={this.handleCamera} | |
active={isVideoOn} | |
/> | |
)} | |
</IconsWrapper> | |
)} | |
</VideoItemWrapper> | |
))} | |
</> | |
) : ( | |
<OuterSpinnerWrapper> | |
<SpinnerWrapper> | |
<SmallSpinner /> | |
</SpinnerWrapper> | |
</OuterSpinnerWrapper> | |
)} | |
</VideoWrapper> | |
</OuterVideoWrapper> | |
) | |
} | |
} | |
export default VideoContainer |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment