This tutorial describes how to create an Agora account and build a sample app with Agora using Electron and React.
- Agora.io Developer Account
- Node.js 6.9.1+ with C++11 support
- Electron 1.8.3+
This section shows you how to prepare and build the Agora Electron wrapper.
To build and run the sample application, first 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 obtain from the dashboard into a text file. You will use this when you launch the app.
Open the settings.js file and add the app ID.
Before
export const APP_ID = ''
After
The <MY_APP_ID>
is the app ID from your Agora dashboard:
export const APP_ID = '<MY_APP_ID>'
Run the install
command in your project directory:
# install dependencies
npm install
Note: During install, the C++ add-on is downloaded instead of being built.
Use the run dev
or run dist
command to build the Agora Electron wrapper.
To enable dynamic compiling and HMR development, use run dev
:
# enable dynamic compiling and HMR developing environment
npm run dev
To build for release, use run dist
:
# build for release
npm run dist
Once the build is complete, use the resulting Agora Electron wrapper to build your application.
The key code for the sample application is in the App.js
file:
Import the required libraries and helper files for the sample application:
Library | Descriptions |
---|---|
React, { Component } |
Enables JSX syntax in ES6 modules |
AgoraRtcEngine |
Agora RTC engine SDK |
{ List } |
Enables use of the list component |
{videoProfileList, audioProfileList, audioScenarioList, APP_ID } |
Constants declared in the settings.js file |
import React, { Component } from 'react';
import AgoraRtcEngine from 'agora-electron-sdk';
import { List } from 'immutable';
import {videoProfileList, audioProfileList, audioScenarioList, APP_ID } from '../utils/settings'
The default class for the application extends the Component
and is named App
.
The remaining code in this section is contained within the class
declaration.
export default class App extends Component {
...
}
- Build the Constructor Method
- Add Functionality to componentDidMount
- Create the subscribeEvents Method
- Create the handleJoin Method
- Create the handleCameraChange Method
- Create the handleMicChange Method
- Create the handleSpeakerChange Method
- Create the handleVideoProfile Method
- Render the View
The constructor()
method passes in the properties parameter props
. This method is called before the view is mounted. Invoke super(props)
first, to ensure this.props
is defined for the class.
Initialize the Agora RTC engine this.rtcEngine
using new AgoraRtcEngine()
.
The remaining code in this section is contained within the constructor()
method.
constructor(props) {
super(props)
this.rtcEngine = new AgoraRtcEngine()
...
}
Do one of the following:
-
If the
APP_ID
is not defined, alert the user that the app ID is missing usingalert()
. -
If the
APP_ID
is defined:- Initialize the Agora RTC engine using
this.rtcEngine.initialize()
. - Set the properties for
state
.
- Initialize the Agora RTC engine using
Property | Value | Description |
---|---|---|
local |
Empty string | Local user ID |
users |
new List() |
List of joined users |
channel |
Empty string | Current channel |
role |
1 |
Current user's role |
videoDevices |
this.rtcEngine.getVideoDevices() |
List of available video devices |
audioDevices |
this.rtcEngine.getAudioRecordingDevices() |
List of available audio devices |
audioPlaybackDevices |
this.rtcEngine.getAudioPlaybackDevices() |
List of audio playback devices |
camera |
0 |
Index of the selected camera |
mic |
0 |
Index of the selected microphone |
speaker |
0 |
Index of the selected speaker |
videoProfile |
43 |
Video profile identifier |
if (!APP_ID) {
alert('APP_ID cannot be empty!')
} else {
this.rtcEngine.initialize(APP_ID)
this.state = {
local: '',
users: new List(),
channel: '',
role: 1,
videoDevices: this.rtcEngine.getVideoDevices(),
audioDevices: this.rtcEngine.getAudioRecordingDevices(),
audioPlaybackDevices: this.rtcEngine.getAudioPlaybackDevices(),
camera: 0,
mic: 0,
speaker: 0,
videoProfile: 43
}
}
The componentDidMount()
is called after the view is mounted in the sample application.
Subscribe to the application event listeners using this.subscribeEvents()
.
componentDidMount() {
this.subscribeEvents()
}
The subscribeEvents
method adds event listeners to the Agora RTC engine using this.rtcEngine.on()
:
subscribeEvents = () => {
...
}
The remaining code in this section is contained within the subscribeEvents
method.
- The
joinedchannel
Event Listener and Callback - The
userjoined
Event Listener and Callback - The
removestream
Event Listener and Callback - The
leavechannel
Event Listener and Callback - The
audiovolumeindication
Event Listener and Callback - The
error
Event Listener and Callback
The joinedchannel
event listener is triggered when a user joins the channel
.
Set the state's local
property to uid
.
this.rtcEngine.on('joinedchannel', (channel, uid, elapsed) => {
this.setState({
local: uid
})
});
The userjoined
event listener is triggered when a new user joins the current channel.
Add uid
to the users list with this.state.users.push()
and set its value to the state's users
property.
this.rtcEngine.on('userjoined', (uid, elapsed) => {
this.setState({
users: this.state.users.push(uid)
})
})
The removestream
event listener is triggered when a user's stream is removed.
Remove uid
from the users list with this.state.users.delete()
and set its value to the state's users
property.
this.rtcEngine.on('removestream', (uid, reason) => {
this.setState({
users: this.state.users.delete(this.state.users.indexOf(uid))
})
})
The leavechannel
event listener is triggered when a user leaves the current channel.
Set the state's local
property to an empty string.
this.rtcEngine.on('leavechannel', () => {
this.setState({
local: ''
})
})
The audiovolumeindication
event listener is triggered when the volume levels change.
Log the following information using console.log()
:
Variable | Description |
---|---|
uid |
User ID |
volume |
Current volume |
speakerNumber |
Index of the speaker device |
totalVolume |
Total volume |
this.rtcEngine.on('audiovolumeindication', (
uid,
volume,
speakerNumber,
totalVolume
) => {
console.log(`uid${uid} volume${volume} speakerNumber${speakerNumber} totalVolume${totalVolume}`)
})
The error
event listener is triggered when an error occurs in the Agora RTC engine.
Log the error information using console.error()
:
this.rtcEngine.on('error', err => {
console.error(err)
})
}
The handleJoin
method initializes the Agora RTC engine settings and joins the user to the channel.
The following methods apply the engine's settings:
Method | Description |
---|---|
setChannelProfile() |
Sets the channel profile |
setClientRole() |
Sets the user role |
setAudioProfile() |
Sets the audio profile |
enableVideo() |
Enables video |
enableLocalVideo(true) |
Enables local video |
enableWebSdkInteroperability(true) |
Enables interoperability between the Agora Native SDK and the Agora Web SDK |
setVideoProfile() |
Sets the video profile |
enableAudioVolumeIndication() |
Enables regular volume indication reports to the application |
Join the channel using rtcEngine.joinChannel()
.
handleJoin = () => {
let rtcEngine = this.rtcEngine
rtcEngine.setChannelProfile(1)
rtcEngine.setClientRole(this.state.role)
rtcEngine.setAudioProfile(0, 1)
rtcEngine.enableVideo()
rtcEngine.enableLocalVideo(true)
rtcEngine.enableWebSdkInteroperability(true)
rtcEngine.setVideoProfile(this.state.videoProfile, false)
rtcEngine.enableAudioVolumeIndication(1000, 3)
rtcEngine.joinChannel(null, this.state.channel, '', Number(`${new Date().getTime()}`.slice(7)))
}
The handleCameraChange
method updates the state's camera
property using this.setState()
.
Retrieve the current video device using this.state.videoDevices[]
. Set the video device using this.rtcEngine.setVideoDevice()
with the current video device's deviceid
.
handleCameraChange = e => {
this.setState({camera: e.currentTarget.value});
this.rtcEngine.setVideoDevice(this.state.videoDevices[e.currentTarget.value].deviceid);
}
The handleMicChange
method updates the state's mic
property using this.setState()
.
Retrieve the current audio recording device using this.state.audioDevices[]
. Set the audio recording device using this.rtcEngine.setAudioRecordingDevice()
with the current audio recording device's deviceid
.
handleMicChange = e => {
this.setState({mic: e.currentTarget.value});
this.rtcEngine.setAudioRecordingDevice(this.state.audioDevices[e.currentTarget.value].deviceid);
}
The handleSpeakerChange
method updates the state's speaker
property using this.setState()
.
Retrieve the current audio playback device using this.state.audioPlaybackDevices[]
. Set the audio playback device using this.rtcEngine.setAudioPlaybackDevice()
with the current audio playback device's deviceid
.
handleSpeakerChange = e => {
this.setState({speaker: e.currentTarget.value});
this.rtcEngine.setAudioPlaybackDevice(this.state.audioPlaybackDevices[e.currentTarget.value].deviceid);
}
The handleVideoProfile
method updates the state's videoProfile
property using this.setState()
:
handleVideoProfile = e => {
this.setState({
videoProfile: Number(e.currentTarget.value)
})
}
The render()
method renders the view for the App
.
Within the return()
, a <div>
element is defined with the class name columns
and the following properties:
Property | Value | Description |
---|---|---|
padding |
20px |
Sets the padding around the element |
height |
100% |
Sets the element to full height |
margin |
0 |
Sets the margin around the element |
The remaining code is contained with the <div>
element.
render() {
return (
<div className="columns" style={{padding: "20px", height: '100%', margin: '0'}}>
...
</div>
)
}
Add a child <div>
element with the class names column
and is-one-quarter
and an overflowY
value of auto
.
<div className="column is-one-quarter" style={{overflowY: 'auto'}}>
...
</div>
- Add the Channel Field
- Add the Role Field
- Add the Video Profile Field
- Add the Audio Profile Fields
- Add the Camera Field
- Add the Microphone Field
- Add the Speaker Field
- Add the Join Button
Add a <div>
element with the class name field
.
Within the <div>
element:
- Add the text
Channel
in a<label>
element with the class namelabel
. - Add a text
<input>
element wrapped in a<div>
element with the class namecontrol
.- Apply an
onChange
event listener to update the state'schannel
property. - Set the
value
property to the state'schannel
property. - Apply the class
input
. - Set the
placeholder
property toInput a channel name
.
- Apply an
<div className="field">
<label className="label">Channel</label>
<div className="control">
<input onChange={e => this.setState({channel: e.currentTarget.value})} value={this.state.channel} className="input" type="text" placeholder="Input a channel name" />
</div>
</div>
Add a <div>
element with the class name field
.
Within the <div>
element:
- Add the text
Role
wrapped in a<label>
element with the class namelabel
. - Add a
<select>
menu element wrapped in a set of nested<div>
elements with the class namescontrol
andselect
.- Apply an
onChange
event listener to update the state'srole
property. - Set the
value
property to the state'srole
property. - Set the
width
to100%
. - Add two role options
Anchor
andAudience
with the values1
and2
, respectively.
- Apply an
<div className="field">
<label className="label">Role</label>
<div className="control">
<div className="select" style={{width: '100%'}}>
<select onChange={e => this.setState({role: e.currentTarget.value})} value={this.state.role} style={{width: '100%'}}>
<option value={1}>Anchor</option>
<option value={2}>Audience</option>
</select>
</div>
</div>
</div>
Add a <div>
element with the class name field
.
Within the <div>
element:
- Add the text
VideoProfile
wrapped in a<label>
element with the class namelabel
. - Add a
<select>
menu element wrapped in a set of nested<div>
elements with the class namescontrol
andselect
.- Apply an
onChange
event listener to invoke thethis.handleVideoProfile
method. - Set the
value
property to the state'svideoProfile
property. - Set the
width
to100%
. - Loop through the video profile list using
videoProfileList.map
, setting the key and value properties toitem.value
and the label toitem.label
.
- Apply an
<div className="field">
<label className="label">VideoProfile</label>
<div className="control">
<div className="select" style={{width: '100%'}}>
<select onChange={this.handleVideoProfile} value={this.state.videoProfile} style={{width: '100%'}}>
{videoProfileList.map(item => (<option key={item.value} value={item.value}>{item.label}</option>))}
</select>
</div>
</div>
</div>
Add a <div>
element with the class name field
.
Within the <div>
element:
- Add the text
AudioProfile
wrapped in a<label>
element with the class namelabel
. - Add two
<select>
menu elements wrapped in a set of nested<div>
elements with the class namescontrol
andselect
.
The first <select>
menu element controls the audio profiles:
- Apply an
onChange
event listener to invoke thethis.handleAudioProfile
method. - Set the
value
property to the state'saudioProfile
property. - Set the
width
to100%
. - Loop through the audio profile list using
audioProfileList.map
, setting the key and value properties toitem.value
and the label toitem.label
.
The second <select>
menu element controls the audio scenarios:
- Apply an
onChange
event listener to invoke thethis.handleAudioScenario
method. - Set the
value
property to the state'saudioScenario
property. - Set the
width
to100%
. - Loop through the audio scenario list using
audioScenarioList.map
, setting the key and value properties toitem.value
and the label toitem.label
.
<div className="field">
<label className="label">AudioProfile</label>
<div className="control">
<div className="select" style={{width: '50%'}}>
<select onChange={this.handleAudioProfile} value={this.state.audioProfile} style={{width: '100%'}}>
{audioProfileList.map(item => (<option key={item.value} value={item.value}>{item.label}</option>))}
</select>
</div>
<div className="select" style={{width: '50%'}}>
<select onChange={this.handleAudioScenario} value={this.state.audioScenario} style={{width: '100%'}}>
{audioScenarioList.map(item => (<option key={item.value} value={item.value}>{item.label}</option>))}
</select>
</div>
</div>
</div>
Add a <div>
element with the class name field
.
Within the <div>
element:
- Add the text
Camera
wrapped in a<label>
element with the class namelabel
. - Add a
<select>
menu element wrapped in a set of nested<div>
elements with the class namescontrol
andselect
.- Apply an
onChange
event listener to invoke thethis.handleCameraChange
method. - Set the
value
property to the state'svideoProfile
property. - Set the
width
to100%
. - Loop through the video devices list using
this.state.videoDevices.map
, setting the key and value properties toindex
and the label toitem.devicename
.
- Apply an
<div className="field">
<label className="label">Camera</label>
<div className="control">
<div className="select" style={{width: '100%'}}>
<select onChange={this.handleCameraChange} value={this.state.camera} style={{width: '100%'}}>
{this.state.videoDevices.map((item, index) => (<option key={index} value={index}>{item.devicename}</option>))}
</select>
</div>
</div>
</div>
Add a <div>
element with the class name field
.
Within the <div>
element:
- Add the text
Microphone
wrapped in a<label>
element with the class namelabel
. - Add a
<select>
menu element wrapped in a set of nested<div>
elements with the class namescontrol
andselect
.- Apply an
onChange
event listener to invoke thethis.handleMicChange
method. - Set the
value
property to the state'smic
property. - Set the
width
to100%
. - Loop through the audio devices list using
this.state.audioDevices.map
, setting the key and value properties toindex
and the label toitem.devicename
.
- Apply an
<div className="field">
<label className="label">Microphone</label>
<div className="control">
<div className="select" style={{width: '100%'}}>
<select onChange={this.handleMicChange} value={this.state.mic} style={{width: '100%'}}>
{this.state.audioDevices.map((item, index) => (<option key={index} value={index}>{item.devicename}</option>))}
</select>
</div>
</div>
</div>
Add a <div>
element with the class name field
.
Within the <div>
element:
- Add the text
Loudspeaker
wrapped in a<label>
element with the class namelabel
. - Add a
<select>
menu element wrapped in a set of nested<div>
elements with the class namescontrol
andselect
.- Apply an
onChange
event listener to invoke thethis.handleSpeakerChange
method. - Set the
value
property to the state'sspeaker
property. - Set the
width
to100%
. - Loop through the audio playback devices list using
this.state.audioPlaybackDevices.map
, setting the key and value properties toindex
and the label toitem.devicename
.
- Apply an
<div className="field">
<label className="label">Loudspeaker</label>
<div className="control">
<div className="select" style={{width: '100%'}}>
<select onChange={this.handleSpeakerChange} value={this.state.speaker} style={{width: '100%'}}>
{this.state.audioPlaybackDevices.map((item, index) => (<option key={index} value={index}>{item.devicename}</option>))}
</select>
</div>
</div>
</div>
Add a <div>
element with the class names field
, is-grouped
, and is-grouped-right
.
Within the <div>
element, add another <div>
element with the class name <control>
.
Add a <button>
element with the class names button
and is-link
in the nested <div>
elements. Add an onClick
handler to the button to invoke this.handleJoin
.
<div className="field is-grouped is-grouped-right">
<div className="control">
<button onClick={this.handleJoin} className="button is-link">Join</button>
</div>
</div>
Add a child <div>
element with the class names column
, is-three-quarters
, and window-container
.
Loop through the users
list using this.state.users.map()
. For each user, add a <Window>
element with the following properties:
Property | Description |
---|---|
key |
String attribute for the list |
uid |
User ID |
rtcEngine |
Agora RTC engine |
local |
Indicates if the user is local |
Determine if the current user is local using this.state.local
. If the current user is local, add a <Window>
element with the same properties as the users list <Window>
elements, excluding the key
property.
<div className="column is-three-quarters window-container">
{this.state.users.map((item, key) => (
<Window key={key} uid={item} rtcEngine={this.rtcEngine} local={false}></Window>
))}
{this.state.local ? (<Window uid={this.state.local} rtcEngine={this.rtcEngine} local={true}>
</Window>) : ''}
</div>
The Window
class extends the Component
class and manages the contents in the browser window.
The constructor()
method passes in the properties parameter props
. This method is called before the Window
view is mounted. Invoke super(props)
first, to ensure this.props
is defined for the class.
The remaining code in this section is contained within the class
declaration.
class Window extends Component {
constructor(props) {
super(props)
this.state = {
loading: false
}
}
...
}
The componentDidMount()
is called after the view is mounted.
Initialize a local variable dom
with the object with the element ID video-${this.props.uid}
.
- If the
local
property anddom
are valid, set up the local video with thedom
usingthis.props.rtcEngine.setupLocalVideo()
. - If the
local
property is invalid anddom
is valid, subscribe the user to the Agora RTC engine usingthis.props.rtcEngine.subscribe()
.
componentDidMount() {
let dom = document.querySelector(`#video-${this.props.uid}`)
if (this.props.local) {
dom && this.props.rtcEngine.setupLocalVideo(dom)
} else {
dom && this.props.rtcEngine.subscribe(this.props.uid, dom)
}
}
The render()
method renders the view for the Window
.
Within the return()
, a <div>
element is defined with the class name window-item
.
Add a secondary <div>
element with the id
value of 'video-' + this.props.uid
and class name video-item
.
render() {
return (
<div className="window-item">
<div className="video-item" id={'video-' + this.props.uid}></div>
</div>
)
}
- Complete API documentation at the Developer Center
- File bugs about this sample
- Basic boilerplate quickstart for the Agora RTC SDK for Electron
- General information about building apps with React and the Electron Webpack
This software is under the MIT License (MIT). View the license.