This repository has been deprecated, please use the demo provided by latest agora react rtc sdk
This tutorial describes how to add video chat to your ReactJS applications using the AgoraRTC SDK.
With this sample app, you can:
- Join a meeting room with up to 7 people
- Leave the meeting room
- Disable/Enable audio
- Disable/Enable video
- Apply two layouts views
- Hide a remote window
- Node.js 6.9.1+
This section shows you how to prepare, build, and run the sample application.
To build and run the sample application, you must obtain an app ID:
- Create a developer account at agora.io. Once you finish the sign-up process, you are redirected to the dashboard.
- Navigate in the dashboard tree on the left to Projects > Project List.
- Copy the app ID that you obtained from the dashboard into a text file. You will use this when you launch the app.
- Edit the
src/agora.config.js
file. In theAGORA_APP_ID
declaration, updateYour App ID
with your app ID.
export const AGORA_APP_ID = 'Your App ID'
-
Download the Agora Web Video SDK. Unzip the downloaded SDK package and copy the
AgoraRTC-*.js
file into the sample application's/src/library/
folder. Rename the file toAgoraRTC.js
.Note: CDN can now be used to retrieve the latest SDK. You do not have to re-download SDK updates.
-
Open the terminal and navigate to your project folder.
cd path/to/project
- Use
npm
to install the dependencies:
# install dependency
npm install
- Build and run the project:
Use start
for a local build. View the application in your browser with the URL http://localhost:3000
# serve with hot reload at localhost:3000
npm run start
Use build
for a production version with minification.
# build for production with minification
npm run build
The sample application uses assets located in the src/assets/images
folder.
The Index page is managed by the src/pages/index/index.jsx file.
The Index page is comprised of four classes:
Class | Description |
---|---|
Index |
Main class for the index page. The other classes are used in the layout for this class. |
InputChannel |
Text input manager for the channel name. |
BaseOptions |
Chooser for the type of call. |
AdvancedOptions |
Chooser for video transcoding. |
import React from 'react'
import * as Cookies from 'js-cookie'
import '../../assets/fonts/css/icons.css'
import Validator from '../../utils/Validator'
import { RESOLUTION_ARR } from '../../utils/Settings'
import './index.css'
class Index extends React.Component {
...
}
class InputChannel extends React.Component {
...
}
class BaseOptions extends React.Component {
...
}
class AdvancedOptions extends React.Component {
...
}
...
export default Index
The Index
class is main class for the index page.
class Index extends React.Component {
...
}
- Add the Constructor Method for the Index Class
- Add Event Listeners for the Index Class
- Add the Render Method for the Index Class
The constructor()
method initializes the state
properties for the class:
Property | Value | Description |
---|---|---|
joinBtn |
false |
Indicates if the Join button is enabled / disabled. |
channel |
Empty string | Name of the channel. |
baseMode |
avc |
Indicates the current base mode for the call. |
transcode |
interop |
Indicates the current transcoding setting. |
attendeeMode |
video |
Indicates the current connection mode for the attendee. |
videoProfile |
480p_4 |
Indicates the current video profile setting. |
constructor(props) {
super(props)
this.state = {
joinBtn: false,
channel: '',
baseMode: 'avc',
transcode: 'interop',
attendeeMode: 'video',
videoProfile: '480p_4',
}
}
The componentDidMount()
method initializes event listener for keyboard presses.
When a keyboard button is pressed, ensure e.keyCode
is the Enter / Return key before executing this.handleJoin()
.
componentDidMount() {
window.addEventListener('keypress', (e) => {
e.keyCode === 13 && this.handleJoin()
})
}
The handleChannel()
method updates the channel
and joinBtn
state properties.
/**
*
* @param {String} val 0-9 a-z A-Z _ only
* @param {Boolean} state
*/
handleChannel = (val, state) => {
this.setState({
channel: val,
joinBtn: state
})
}
The handleJoin()
method initiates joining the channel.
- Ensure the
joinBtn
state isfalse
, otherwise stop the method execution with areturn
. - Set a log for the
state
information usingconsole.log
. - Set cookies for the state properties
channel
,baseMode
,transcode
,attendeeMode
, andvideoProfile
. - Transfer the user to the
meeting
page.
handleJoin = () => {
if (!this.state.joinBtn) {
return
}
console.log(this.state)
Cookies.set('channel', this.state.channel)
Cookies.set('baseMode', this.state.baseMode)
Cookies.set('transcode', this.state.transcode)
Cookies.set('attendeeMode', this.state.attendeeMode)
Cookies.set('videoProfile', this.state.videoProfile)
window.location.hash = "meeting"
}
The render()
method displays the UI for the index page in the return()
.
The layout is comprised of three sections, specified by the class names ag-header
, ag-main
, and ag-footer
.
The ag-main
section contains the key login elements for the layout. The remaining code for this section is contained within the section
sub-element.
The ag-footer
section contains a Powered By Agora
text and Agora contact information.
render() {
return (
<div className="wrapper index">
<div className="ag-header"></div>
<div className="ag-main">
<section className="login-wrapper">
...
</section>
</div>
<div className="ag-footer">
<a className="ag-href" href="https://www.agora.io">
<span>Powered By Agora</span>
</a>
<div>
<span>Interested in Agora video call SDK? Contact </span>
<span className="ag-contact">sales@agora.io</span>
</div>
</div>
</div>
)
}
The login-header
section contains the Agora logo, application title, and the application motto / subtitle.
<div className="login-header">
<img src={require('../../assets/images/ag-logo.png')} alt="" />
<p className="login-title">AgoraWeb v2.1</p>
<p className="login-subtitle">Powering Real-Time Communications</p>
</div>
The login-body
section is divided into three main sections:
- The text input for the room name
- The call options
- The attendee mode options
<div className="login-body">
<div className="columns">
<div className="column is-12">
...
</div>
</div>
<div className="columns">
<div className="column is-7">
...
</div>
<div className="column is-5">
...
</div>
</div>
<div className="columns">
<div className="column">
...
</div>
</div>
</div>
The first section is a text input box for the room name / channel. The InputChannel
element is nested within two div
elements. The text input has onChange
event listener which invokes the this.handleChannel
method.
<div className="columns">
<div className="column is-12">
<InputChannel onChange={this.handleChannel} placeholder="Input a room name here"></InputChannel>
</div>
</div>
The second section is contains a BaseOptions
component and a AdvancedOptions
component.
The BaseOptions
component has an onChange
event listener which updates the baseMode
state value.
The AdvancedOptions
component has two event listeners:
- An
onRadioChange
event listener which updates thetranscode
state value. - An
OnSelectChange
event listener which updates thevideoProfile
state value.
<div className="columns">
<div className="column is-7">
<BaseOptions
onChange={val => this.setState({ baseMode: val })}>
</BaseOptions>
</div>
<div className="column is-5">
<AdvancedOptions
onRadioChange={val => this.setState({ transcode: val })}
onSelectChange={val => this.setState({ videoProfile: val })}>
</AdvancedOptions>
</div>
</div>
The third section is contains a set of radio dials and supporting images and text labels. Each radio input
element has an onChange
event listener which updates the attendeeMode
state value.
<div className="columns">
<div className="column">
<div id="attendeeMode" className="control">
<label className="radio">
<input onChange={e => this.setState({ attendeeMode: e.target.value })}
value="video" type="radio"
name="attendee" defaultChecked />
<span className="radio-btn">
</span>
<span className="radio-img video">
</span>
<span className="radio-msg">Video Call : join with video call</span>
</label>
<br />
<label className="radio">
<input onChange={e => this.setState({ attendeeMode: e.target.value })}
value="audio-only" type="radio"
name="attendee" />
<span className="radio-btn">
</span>
<span className="radio-img audio">
</span>
<span className="radio-msg">Audio-only : join with audio call</span>
</label>
<br />
<label className="radio">
<input onChange={e => this.setState({ attendeeMode: e.target.value })}
value="audience" type="radio"
name="attendee" />
<span className="radio-btn">
</span>
<span className="radio-img audience">
</span>
<span className="radio-msg">Audience : join as an audience</span>
</label>
</div>
</div>
</div>
The footer for the ag-main
section contains a Join button, designated by an a
element. The Join button has an onClick
event listener which invokes the this.handleJoin
method. This button is set to enabled / disabled by the joinBtn
state value.
<div className="login-footer">
<a id="joinBtn"
onClick={this.handleJoin}
disabled={!this.state.joinBtn}
className="ag-rounded button is-info">Join
</a>
</div>
The InputChannel
class defines the room channel text input for the index page.
class InputChannel extends React.Component {
...
}
- Add the Constructor Method for the InputChannel Class
- Add Validation and Event Listeners for the InputChannel Class
- Add the Render Method for the InputChannel Class
The constructor()
method initializes the state properties errorMsg
and state
to empty strings.
constructor(props) {
super(props)
this.state = {
errorMsg: '',
state: ''
}
}
The validate()
method checks the string for the text input and updates the state
and errorMsg
properties if necessary.
Set both properties to an empty string.
validate = (val) => {
this.setState({
state: '',
errorMsg: ''
})
...
}
Ensure the text input value is not empty using Validator.isNonEmpty
. If the value is empty, update the errorMsg
and state
properties to inform the user that the text input should not be empty.
if (Validator.isNonEmpty(val.trim())) {
this.setState({
errorMsg: 'Cannot be empty!',
state: 'is-danger'
})
return false
}
Ensure the text input value at least 1
character long. If the value is shorter than 1
character, update the errorMsg
and state
properties to inform the user that the text input should be longer than 1
character.
else if (Validator.minLength(val.trim(), 1)) {
this.setState({
errorMsg: 'No shorter than 1!',
state: 'is-danger'
})
return false
}
Ensure the text input value 16
characters or shorter. If the value is longer than 16
characters, update the errorMsg
and state
properties to inform the user that the text input should no longer than 16
characters.
else if (Validator.maxLength(val.trim(), 16)) {
this.setState({
errorMsg: 'No longer than 16!',
state: 'is-danger'
})
return false
}
Ensure the text input value only contains valid characters. If invalid characters are found, update the errorMsg
and state
properties to inform the user that the text input should only contain letters, numbers, or the _
character.
else if (Validator.validChar(val.trim())) {
this.setState({
state: 'is-danger',
errorMsg: 'Only capital or lower-case letter, number and "_" are permitted!'
})
return false
}
If the text input value passes all the validation tests, update the state
property to is-success
and return true
.
else {
this.setState({
state: 'is-success'
})
return true
}
The handleChange()
method is invoked when the text input value is updated. Retrieve the state
using this.validate()
. Update the property change for the component using this.props.onChange()
handleChange = (e) => {
let state = this.validate(e.target.value)
this.props.onChange(e.target.value, state)
}
The render()
method displays the UI for the InputChannel
component.
Set the validateIcon
value, based on the component's state
property.
render() {
let validateIcon = ''
switch (this.state.state) {
default:
case '':
validateIcon = ''; break;
case 'is-success':
validateIcon = (<i className="ag-icon ag-icon-valid"></i>); break;
case 'is-danger':
validateIcon = (<i className="ag-icon ag-icon-invalid"></i>); break;
}
...
}
The return
contains the layout for the component. The key elements are comprised of the text input box, login button, validation icon, and validation error message.
The text input
element is initialized with the component placeholder
property. The onInput
event listener triggers the this.handleChange
method.
The validation icon and error message are updated as the text input box value is validated.
return (
<div className="channel-wrapper control has-icons-left">
<input onInput={this.handleChange}
id="channel"
className={'ag-rounded input ' + this.state.state}
type="text"
placeholder={this.props.placeholder} />
<span className="icon is-small is-left">
<img src={require('../../assets/images/ag-login.png')} alt="" />
</span>
<span className="validate-icon">
{validateIcon}
</span>
<div className="validate-msg">
{this.state.errorMsg}
</div>
</div>
)
The BaseOptions
class defines the base options component for the index page.
class BaseOptions extends React.Component {
...
}
- Add the Constructor Method for the BaseOptions Class
- Add the Event Listener for the BaseOptions Class
- Add the Render Method for the BaseOptions Class
The constructor()
method initializes the _options
array and state properties active
and message
.
constructor(props) {
super(props)
this._options = [
{
label: 'Agora Video Call',
value: 'avc',
content: 'One to one and group calls'
},
{
label: 'Agora Live',
value: 'al',
content: 'Enabling real-time interactions between the host and the audience'
}
]
this.state = {
active: false,
message: 'Agora Video Call',
}
}
The handleSelect()
method is invoked when a dropdown menu item is selected.
Update the state properties message
and active
with the selection values, and trigger the component's onChange
event listener with val
.
handleSelect = (item) => {
let msg = item.label
let val = item.value
this.setState({
'message': msg,
'active': false
})
this.props.onChange(val)
}
The render()
method displays the UI for the BaseOptions
component.
Create the options
layout variable, by mapping through this._options
, giving each option an item label
and content
description wrapped in a div
element.
Add an onClick
event listener, which triggers the this.handleSelect()
method for the item selection.
render() {
const options = this._options.map((item, index) => {
return (
<div className="dropdown-item"
key={index}
onClick={(e) => this.handleSelect(item, e)}>
<p>{item.label}</p>
<hr />
<p>{item.content}</p>
</div>
)
})
The return
contains the layout for the component. The component is enabled / disabled based on the state's active
property.
When the dropdown button dropdown-trigger
is clicked, set the dropdown menu to open/closed by updating the active
property using this.setState()
. The selection option is updated in the baseOptionLabel
element using the message
state property.
The options
layout variable populates the items in the dropdown menu.
return (
<div className={this.state.active ? 'dropdown is-active' : 'dropdown'}>
<div className="dropdown-trigger"
onClick={() => this.setState({ 'active': !this.state.active })}>
<a id="baseMode" className="ag-rounded button" aria-haspopup="true" aria-controls="baseModeOptions">
<span id="baseOptionLabel">{this.state.message}</span>
<span className="icon is-small">
<i className="ag-icon ag-icon-arrow-down" aria-hidden="true"></i>
</span>
</a>
</div>
<div className="dropdown-menu" id="baseModeOptions" role="menu">
<div className="dropdown-content">
{options}
</div>
</div>
</div>
)
}
The AdvancedOptions
class defines the advanced options component for the index page.
class AdvancedOptions extends React.Component {
...
}
- Add the Constructor Method for the AdvancedOptions Class
- Add Event Listeners for AdvancedOptions Class
- Add the Render Method for the AdvancedOptions Class
The constructor()
method initializes the state property active
to false
.
constructor(props) {
super(props)
this.state = {
active: false,
}
}
The handleRadio()
method invokes the component's onRadioChange()
method with the target value
.
handleRadio = (e) => {
this.props.onRadioChange(e.target.value)
}
The handleSelect()
method invokes the component's onSelectChange()
method with the target value
.
handleSelect = (e) => {
this.props.onSelectChange(e.target.value)
}
The render()
method displays the UI for the AdvancedOptions
component.
Create the options
layout variable, by mapping through Object.entries
, returning an option
element with a specified key
, value
, and description for each item.
render() {
const options = Object.entries(RESOLUTION_ARR).map((item, index) => {
return (
<option key={index} value={item[0].split(",")[0]}>{item[1][0]}x {item[1][1]}, {item[1][2]}fps, {item[1][3]}kbps</option>
)
})
...
}
The return
contains the layout for the component. The component is enabled / disabled based on the state's active
property.
return (
<div className={this.state.active ? 'dropdown is-active' : 'dropdown'}>
...
</div>
)
When the dropdown button dropdown-trigger
is clicked, set the dropdown menu to open/closed by updating the active
property using this.setState()
.
<div className="dropdown-trigger"
onClick={() => this.setState({ 'active': !this.state.active })}>
<a id="advancedProfile" className="ag-rounded button" aria-haspopup="true" aria-controls="advancedOptions">
<span>Advanced</span>
</a>
</div>
The advanced options dropdown menu contains transcoding radio
input options and video profile options.
The transcoding radio options have an onChange
event listener to invoke the this.handleRadio
method.
The video profile selection menu is populated by the options
layout variable. This menu has an onChange
event listener which triggers the this.handleSelect
method.
<div className="dropdown-menu" id="advancedOptions" role="menu">
<div className="dropdown-content">
<div className="dropdown-item">
<div className="control">
<label className="radio">
<input value="" type="radio" name="transcode" onChange={this.handleRadio} />
<span>VP8-only</span>
</label>
<label className="radio">
<input value="interop" type="radio" defaultChecked onChange={this.handleRadio} name="transcode" />
<span>VP8 & H264</span>
</label>
<label className="radio">
<input value="h264_interop" type="radio" onChange={this.handleRadio} name="transcode" />
<span>H264-only</span>
</label>
</div>
</div>
<div className="dropdown-item">
<div className="select is-rounded">
<select onChange={this.handleSelect} defaultValue="480p_4" id="videoProfile" className="ag-rounded is-clipped">
{options}
</select>
</div>
</div>
</div>
</div>
The Meeting page is managed by the src/pages/meeting/index.jsx file.
The Meeting
class defines the layout and controls for the meeting page.
import React from 'react'
import * as Cookies from 'js-cookie'
import './meeting.css'
import AgoraVideoCall from '../../components/AgoraVideoCall'
import {AGORA_APP_ID} from '../../agora.config'
class Meeting extends React.Component {
...
}
export default Meeting
The constructor()
method sets the variables for the page.
The following variables are set using cookies, that were set from the index page. If the cookie value isn't found, a default value is used:
Variable|Cookie value|Default value|Description
---|---|---
videoProfile
|videoProfile
|480p_4
|Video profile used for the connection.
channel
|channel
|test
|Channel name.
transcode
|transcode
|interop
|Video transcoding mode.
attendeeMode
|attendeeMode
|video
|Attendee mode.
baseMode
|baseMode
|avc
|Base mode for the connection.
Ensure this.appId
is valid; otherwise display an alert()
that an App ID is needed and stop execution.
Set this.uid
to undefined
.
constructor(props) {
super(props)
this.videoProfile = (Cookies.get('videoProfile')).split(',')[0] || '480p_4',
this.channel = Cookies.get('channel') || 'test',
this.transcode = Cookies.get('transcode') || 'interop',
this.attendeeMode = Cookies.get('attendeeMode') || 'video',
this.baseMode = Cookies.get('baseMode') || 'avc'
this.appId = AGORA_APP_ID
if (!this.appId) {
return alert('Get App ID first!')
}
this.uid = undefined
}
The render()
method displays the UI for the AdvancedOptions
component in the return
. The layout is broken up into three main sections ag-header
, ag-main
, and ag-footer
.
The header contains:
- The Agora logo.
- The application name and version.
- The room name / channel.
The main section contains the AgoraVideoCall
component, and sets the properties based on the variables initialized in the constructor.
The footer section contains a Powered By Agora
text, and Agora contact information.
render() {
return (
<div className="wrapper meeting">
<div className="ag-header">
<div className="ag-header-lead">
<img className="header-logo" src={require('../../assets/images/ag-logo.png')} alt="" />
<span>AgoraWeb v2.1</span>
</div>
<div className="ag-header-msg">
Room: <span id="room-name">{this.channel}</span>
</div>
</div>
<div className="ag-main">
<div className="ag-container">
<AgoraVideoCall
videoProfile={this.videoProfile}
channel={this.channel}
transcode={this.transcode}
attendeeMode={this.attendeeMode}
baseMode={this.baseMode}
appId={this.appId}
uid={this.uid}></AgoraVideoCall>
</div>
</div>
<div className="ag-footer">
<a className="ag-href" href="https://www.agora.io"><span>Powered By Agora</span></a>
<span>Talk to Support: 400 632 6626</span>
</div>
</div>
)
}
The AgoraVideoCall component is managed by the src/components/AgoraVideoCall/index.jsx file.
import React from 'react'
import { merge } from 'lodash'
import './canvas.css'
import '../../assets/fonts/css/icons.css'
- Initialize the AgoraVideoCall Component
- Add the Constructor Method for the AgoraCanvas Class
- Add Mount Methods for the AgoraCanvas Class
- Add the Update Method for the AgoraCanvas Class
- Add the Render Method for the AgoraCanvas Class
- Add Custom Stream Methods for the AgoraCanvas Class
- Add Other Custom Methods for the AgoraCanvas Class
The tile_canvas
constant defines how to tile the video layout for the call.
const tile_canvas = {
'1': ['span 12/span 24'],
'2': ['span 12/span 12/13/25', 'span 12/span 12/13/13'],
'3': ['span 6/span 12', 'span 6/span 12', 'span 6/span 12/7/19'],
'4': ['span 6/span 12', 'span 6/span 12', 'span 6/span 12', 'span 6/span 12/7/13'],
'5': ['span 3/span 4/13/9', 'span 3/span 4/13/13', 'span 3/span 4/13/17', 'span 3/span 4/13/21', 'span 9/span 16/10/21'],
'6': ['span 3/span 4/13/7', 'span 3/span 4/13/11', 'span 3/span 4/13/15', 'span 3/span 4/13/19', 'span 3/span 4/13/23', 'span 9/span 16/10/21'],
'7': ['span 3/span 4/13/5', 'span 3/span 4/13/9', 'span 3/span 4/13/13', 'span 3/span 4/13/17', 'span 3/span 4/13/21', 'span 3/span 4/13/25', 'span 9/span 16/10/21'],
}
The AgoraCanvas
class defines the layout and controls for the AgoraCanvas
component.
/**
* @prop appId uid
* @prop transcode attendeeMode videoProfile channel baseMode
*/
class AgoraCanvas extends React.Component {
...
}
export default AgoraCanvas
The constructor()
method initializes the client
, localStream
, shareClient
, and shareStream
variables and the state properties displayMode
, streamList
, and readyState
.
constructor(props) {
super(props)
this.client = {}
this.localStream = {}
this.shareClient = {}
this.shareStream = {}
this.state = {
displayMode: 'pip',
streamList: [],
readyState: false
}
}
The componentWillMount()
method initializes the Agora RTC engine and joins the user to the specified channel.
Create the Agora RTC client using AgoraRTC.createClient()
.
componentWillMount() {
let $ = this.props
// init AgoraRTC local client
this.client = AgoraRTC.createClient({ mode: $.transcode })
...
}
- Initialize the Agora RTC client using
this.client.init()
and log the initialization confirmation usingconsole.log()
. - Subscribe to the stream events using
this.subscribeStreamEvents()
. - Join the channel using
this.client.join()
and log the join confirmation usingconsole.log()
.
The next section of code occurs within the join()
method completion.
this.client.init($.appId, () => {
console.log("AgoraRTC client initialized")
this.subscribeStreamEvents()
this.client.join($.appId, $.channel, $.uid, (uid) => {
console.log("User " + uid + " join channel successfully")
console.log('At ' + new Date().toLocaleTimeString())
...
})
})
-
Create the local stream using
this.streamInit()
. -
Initialize the local stream using
this.localStream.init()
.- If the initialization is successful, add the local stream using
this.addStream()
and publish the stream usingthis.client.publish()
. - If the initialization is not successful, log the error using
console.log()
and update thereadyState
state property.
- If the initialization is successful, add the local stream using
// create local stream
// It is not recommended to setState in function addStream
this.localStream = this.streamInit(uid, $.attendeeMode, $.videoProfile)
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 })
})
The componentDidMount()
method initializes an event listener for the control button area.
-
Declare
canvas
andbtnGroup
to reference the layout canvas and buttons. -
Add a
mousemove
event listener to the canvas usingcanvas.addEventListener()
.- If
global._toolbarToggle
is true, clear the toolbar timer usingclearTimeout()
. - Add the
active
class tobtnGroup
usingclassList.add()
. - Start a new
2000
millisecond timer usingsetTimeout()
, which will remove theactive
class frombtnGroup
usingclassList.remove()
.
- If
componentDidMount() {
// add listener to control btn group
let canvas = document.querySelector('#ag-canvas')
let btnGroup = document.querySelector('.ag-btn-group')
canvas.addEventListener('mousemove', () => {
if (global._toolbarToggle) {
clearTimeout(global._toolbarToggle)
}
btnGroup.classList.add('active')
global._toolbarToggle = setTimeout(function () {
btnGroup.classList.remove('active')
}, 2000)
})
}
The componentWillUnmount()
method closes the stream and exits the channel.
- If
this.client
is valid, unpublish the local stream usingthis.client.unpublish()
. - If
this.localStream
is valid, close the local stream usingthis.localStream.close()
. - If
this.client
is valid, leave the channel usingthis.client.leave()
and log the results usingconsole.log()
.
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.')
})
}
The componentDidUpdate()
method triggers when the component updates.
Declare canvas
to reference the updated layout canvas.
componentDidUpdate() {
// rerendering
let canvas = document.querySelector('#ag-canvas')
...
}
If this.state.displayMode
is in pip
mode, ensure the number of streams is greater than 4
; otherwise set the displayMode
state to tile
and end execution.
For each item in this.state.streamList
:
- Retrieve the ID of the item using
item.getId()
. - Retrieve the UI element for the item using
document.querySelector()
. - Ensure the UI element exists:
- Create a new
section
element usingdocument.createElement()
. - Set the
id
andclass
attributes for the element usingdom.setAttribute()
. - Append the element to the
canvas
usingappendChild()
. - Play the stream using
item.play()
.
- Create a new
- Set the
style
attribute usingdom.setAttribute()
, based on if the item is the last element inthis.state.streamList
. - Ensure
item.player.resize
is valid and resize the player usingitem.player.resize()
.
// pip mode (can only use when less than 4 people in channel)
if (this.state.displayMode === 'pip') {
let no = this.state.streamList.length
if (no > 4) {
this.setState({ displayMode: 'tile' })
return
}
this.state.streamList.map((item, index) => {
let id = item.getId()
let dom = document.querySelector('#ag-item-' + id)
if (!dom) {
dom = document.createElement('section')
dom.setAttribute('id', 'ag-item-' + id)
dom.setAttribute('class', 'ag-item')
canvas.appendChild(dom)
item.play('ag-item-' + id)
}
if (index === no - 1) {
dom.setAttribute('style', `grid-area: span 12/span 24/13/25`)
}
else {
dom.setAttribute('style', `grid-area: span 3/span 4/${4 + 3 * index}/25;
z-index:1;width:calc(100% - 20px);height:calc(100% - 20px)`)
}
item.player.resize && item.player.resize()
})
}
If this.state.displayMode
is in tile
mode, retrieve the number of streams using this.state.streamList.length
.
For each item in this.state.streamList
:
- Retrieve the ID of the item using
item.getId()
. - Retrieve the UI element for the item using
document.querySelector()
. - Ensure the UI element exists:
- Create a new
section
element usingdocument.createElement()
. - Set the
id
andclass
attributes for the element usingdom.setAttribute()
. - Append the element to the
canvas
usingappendChild()
. - Play the stream using
item.play()
.
- Create a new
- Set the
style
attribute usingdom.setAttribute()
. - Ensure
item.player.resize
is valid and resize the player usingitem.player.resize()
.
// tile mode
else if (this.state.displayMode === 'tile') {
let no = this.state.streamList.length
this.state.streamList.map((item, index) => {
let id = item.getId()
let dom = document.querySelector('#ag-item-' + id)
if (!dom) {
dom = document.createElement('section')
dom.setAttribute('id', 'ag-item-' + id)
dom.setAttribute('class', 'ag-item')
canvas.appendChild(dom)
item.play('ag-item-' + id)
}
dom.setAttribute('style', `grid-area: ${tile_canvas[no][index]}`)
item.player.resize && item.player.resize()
})
}
If this.state.displayMode
is in tbd
mode, do nothing as screen share mode has not been enabled in this sample application.
// screen share mode (tbd)
else if (this.state.displayMode === 'share') {
}
The render()
method displays the UI for the AgoraCanvas
.
render() {
...
}
The style
constant provides style settings for the layout.
const style = {
display: 'grid',
gridGap: '10px',
alignItems: 'center',
justifyItems: 'center',
gridTemplateRows: 'repeat(12, auto)',
gridTemplateColumns: 'repeat(24, auto)'
}
The videoControlBtn
constant provides a UI button to enable / disable video, if the attendeeMode
is set to video
.
The button has an onClick
event listener that invokes the this.handleCamera
method.
const videoControlBtn = this.props.attendeeMode === 'video' ?
(<span
onClick={this.handleCamera}
className="ag-btn videoControlBtn"
title="Enable/Disable Video">
<i className="ag-icon ag-icon-camera"></i>
<i className="ag-icon ag-icon-camera-off"></i>
</span>) : ''
The audioControlBtn
constant provides a UI button to enable / disable audio, if the attendeeMode
is set to audience
.
The button has an onClick
event listener that invokes the this.handleMic
method.
const audioControlBtn = this.props.attendeeMode !== 'audience' ?
(<span
onClick={this.handleMic}
className="ag-btn audioControlBtn"
title="Enable/Disable Audio">
<i className="ag-icon ag-icon-mic"></i>
<i className="ag-icon ag-icon-mic-off"></i>
</span>) : ''
The switchDisplayBtn
constant provides a UI button to switch display modes.
The button has an onClick
event listener that invokes the this.switchDisplay
method.
This button is disabled if this.state.streamList.length
is greater than 4
; otherwise it is enabled.
const switchDisplayBtn = (
<span
onClick={this.switchDisplay}
className={this.state.streamList.length > 4 ? "ag-btn displayModeBtn disabled" : "ag-btn displayModeBtn"}
title="Switch Display Mode">
<i className="ag-icon ag-icon-switch-display"></i>
</span>
)
The hideRemoteBtn
constant provides a UI button to hide the remote stream.
This button is disabled if this.state.streamList.length
is greater than 4
or this.state.displayMode
does not equal pip
; otherwise it is enabled.
The button has an onClick
event listener that invokes the this.hideRemote
method.
const hideRemoteBtn = (
<span
className={this.state.streamList.length > 4 || this.state.displayMode !== 'pip' ? "ag-btn disableRemoteBtn disabled" : "ag-btn disableRemoteBtn"}
onClick={this.hideRemote}
title="Hide Remote Stream">
<i className="ag-icon ag-icon-remove-pip"></i>
</span>
)
The exitBtn
constant provides a UI button to exit the room / channel.
The button has an onClick
event listener that invokes the this.handleExit
method.
This button is enabled if this.state.readyState
is true
; otherwise it is disabled.
const exitBtn = (
<span
onClick={this.handleExit}
className={this.state.readyState ? 'ag-btn exitBtn' : 'ag-btn exitBtn disabled'}
title="Exit">
<i className="ag-icon ag-icon-leave"></i>
</span>
)
The return
provides the layout for the AgoraCanvas
component.
Add the exitBtn
, videoControlBtn
, audioControlBtn
, switchDisplayBtn
, and hideRemoteBtn
layout values in a set of nested div
elements.
return (
<div id="ag-canvas" style={style}>
<div className="ag-btn-group">
{exitBtn}
{videoControlBtn}
{audioControlBtn}
{/* <span className="ag-btn shareScreenBtn" title="Share Screen">
<i className="ag-icon ag-icon-screen-share"></i>
</span> */}
{switchDisplayBtn}
{hideRemoteBtn}
</div>
</div>
)
The streamInit()
method initializes a video stream.
- Initialize a default configuration object with
streamID
,audio
,video
, andscreen
properties. - If the
attendeeMode
is audio only, setdefaultConfig.video
tofalse
. If theattendeeMode
isaudience
, setdefaultConfig.video
anddefaultConfig.audio
tofalse
. - Create a stream with the configuration using
AgoraRTC.createStream()
. - Set the video profile using
stream.setVideoProfile()
and return the resultingstream
.
streamInit = (uid, attendeeMode, videoProfile, config) => {
let defaultConfig = {
streamID: uid,
audio: true,
video: true,
screen: false
}
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(merge(defaultConfig, config))
stream.setVideoProfile(videoProfile)
return stream
}
The subscribeStreamEvents()
method adds event listeners to the streams:
Event Listener | Description | Execution after trigger |
---|---|---|
stream-added |
Triggers when a stream is added. | Add logs for the stream using console.log and subscribe the stream to the client using rt.client.subscribe() . |
peer-leave |
Triggers when a peer leaves the room / channel. | Add logs for the stream using console.log and remove the stream using rt.removeStream() . |
stream-subscribed |
Triggers when a stream is subscribed to the client. | Add logs for the stream using console.log and add the stream using rt.addStream() . |
stream-removed |
Triggered when a stream is removed from the client. | Add logs for the stream using console.log and remove the stream using rt.removeStream() . |
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())
})
}
The removeStream()
method, removes the stream for the specified uid
.
For each stream in this.state.streamList
, find the stream ID that matches uid
.
- Close the item using
item.close()
. - Retrieve the layout element using
document.querySelector()
. If the element is valid, remove it from the layout usingelement.parentNode.removeChild()
. - Create a
tempList
object for the list of streams and remove the current stream usingtempList.splice()
. - Update the
streamList
state property withtempList
.
removeStream = (uid) => {
this.state.streamList.map((item, index) => {
if (item.getId() === uid) {
item.close()
let element = document.querySelector('#ag-item-' + uid)
if (element) {
element.parentNode.removeChild(element)
}
let tempList = [...this.state.streamList]
tempList.splice(index, 1)
this.setState({
streamList: tempList
})
}
})
}
The addStream()
method adds a stream.
Ensure the added stream does not exist in this.state.streamList
before continuing execution.
If push
is true
, append stream
to the stream list using this.state.streamList.concat()
and update the streamList
state property.
Otherwise, prepend stream
to the stream list using [stream].concat()
and update the streamList
state property.
addStream = (stream, push = false) => {
let repeatition = this.state.streamList.some(item => {
return item.getId() === stream.getId()
})
if (repeatition) {
return
}
if (push) {
this.setState({
streamList: this.state.streamList.concat([stream])
})
}
else {
this.setState({
streamList: [stream].concat(this.state.streamList)
})
}
}
The handleCamera()
method enables / disables the camera.
- Update the class for the trigger target using
e.currentTarget.classList.toggle()
. - Disable or enable the video using
this.localStream.disableVideo()
orthis.localStream.enableVideo()
based onthis.localStream.isVideoOn()
.
handleCamera = (e) => {
e.currentTarget.classList.toggle('off')
this.localStream.isVideoOn() ?
this.localStream.disableVideo() : this.localStream.enableVideo()
}
The handleMic()
method enables / disables the microphone / audio.
- Update the class for the trigger target using
e.currentTarget.classList.toggle()
. - Disable or enable the audio using
this.localStream.disableAudio()
orthis.localStream.enableAudio()
based onthis.localStream.isAudioOn()
.
handleMic = (e) => {
e.currentTarget.classList.toggle('off')
this.localStream.isAudioOn() ?
this.localStream.disableAudio() : this.localStream.enableAudio()
}
The switchDisplay()
method switches displays.
Ensure the trigger target is not disabled e.currentTarget.classList.contains()
and the stream list is greater than 1
before continuing execution.
Update the displayMode
state property using this.setState()
based on value of this.state.displayMode
:
Current Display Mode Value | New Display Mode Value |
---|---|
pip |
tile |
tile |
php |
share |
No change |
Other | Log an error using console.error() . |
switchDisplay = (e) => {
if (e.currentTarget.classList.contains('disabled') || this.state.streamList.length <= 1) {
return
}
if (this.state.displayMode === 'pip') {
this.setState({ displayMode: 'tile' })
}
else if (this.state.displayMode === 'tile') {
this.setState({ displayMode: 'pip' })
}
else if (this.state.displayMode === 'share') {
// do nothing or alert, tbd
}
else {
console.error('Display Mode can only be tile/pip/share')
}
}
The hideRemote()
method hides the remote stream.
Ensure the trigger target is not disabled e.currentTarget.classList.contains()
and the stream list is greater than 1
before continuing execution.
- Declare
list
- Retrieve the ID of the last stream in the list using
getId()
. - Update
list
with the array of all layout items matching the ID usingArray.from()
. - For each item in
list
, update the display usingitem.style.display
. The value of thedisplay
isnone
ifitem.style.display
equalsnone
; otherwise the value isblock
.
hideRemote = (e) => {
if (e.currentTarget.classList.contains('disabled') || this.state.streamList.length <= 1) {
return
}
let list
let id = this.state.streamList[this.state.streamList.length - 1].getId()
list = Array.from(document.querySelectorAll(`.ag-item:not(#ag-item-${id})`))
list.map(item => {
if (item.style.display !== 'none') {
item.style.display = 'none'
}
else {
item.style.display = 'block'
}
})
}
The handleExit()
method exits the room / channel.
Ensure the trigger target is not disabled e.currentTarget.classList.contains()
before continuing execution.
- Ensure
this.client
is valid and unpublish the stream usingthis.client.unpublish()
- Ensure
this.localStream
is valid and close the stream usingthis.localStream.close()
- Ensure
this.client
is valid and leave the room / channel usingthis.client.leave()
- Update the
readyState
state property tofalse
. - Set
this.client
andthis.localStream
tonull
. - Redirect the window to the index page using
window.location.hash
.
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
// redirect to index
window.location.hash = ''
}
}
- Find full API documentation in the Document Center.
- File bugs about this sample.
- Agora Video SDK sample OpenVideoCall for Vue is also available.
This software is licensed under the MIT License (MIT). View the license.