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 devTo build for release, use run dist:
# build for release
npm run distOnce 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_IDis not defined, alert the user that the app ID is missing usingalert(). -
If the
APP_IDis 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
joinedchannelEvent Listener and Callback - The
userjoinedEvent Listener and Callback - The
removestreamEvent Listener and Callback - The
leavechannelEvent Listener and Callback - The
audiovolumeindicationEvent Listener and Callback - The
errorEvent 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
Channelin a<label>element with the class namelabel. - Add a text
<input>element wrapped in a<div>element with the class namecontrol.- Apply an
onChangeevent listener to update the state'schannelproperty. - Set the
valueproperty to the state'schannelproperty. - Apply the class
input. - Set the
placeholderproperty 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
Rolewrapped in a<label>element with the class namelabel. - Add a
<select>menu element wrapped in a set of nested<div>elements with the class namescontrolandselect.- Apply an
onChangeevent listener to update the state'sroleproperty. - Set the
valueproperty to the state'sroleproperty. - Set the
widthto100%. - Add two role options
AnchorandAudiencewith the values1and2, 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
VideoProfilewrapped in a<label>element with the class namelabel. - Add a
<select>menu element wrapped in a set of nested<div>elements with the class namescontrolandselect.- Apply an
onChangeevent listener to invoke thethis.handleVideoProfilemethod. - Set the
valueproperty to the state'svideoProfileproperty. - Set the
widthto100%. - Loop through the video profile list using
videoProfileList.map, setting the key and value properties toitem.valueand 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
AudioProfilewrapped in a<label>element with the class namelabel. - Add two
<select>menu elements wrapped in a set of nested<div>elements with the class namescontrolandselect.
The first <select> menu element controls the audio profiles:
- Apply an
onChangeevent listener to invoke thethis.handleAudioProfilemethod. - Set the
valueproperty to the state'saudioProfileproperty. - Set the
widthto100%. - Loop through the audio profile list using
audioProfileList.map, setting the key and value properties toitem.valueand the label toitem.label.
The second <select> menu element controls the audio scenarios:
- Apply an
onChangeevent listener to invoke thethis.handleAudioScenariomethod. - Set the
valueproperty to the state'saudioScenarioproperty. - Set the
widthto100%. - Loop through the audio scenario list using
audioScenarioList.map, setting the key and value properties toitem.valueand 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
Camerawrapped in a<label>element with the class namelabel. - Add a
<select>menu element wrapped in a set of nested<div>elements with the class namescontrolandselect.- Apply an
onChangeevent listener to invoke thethis.handleCameraChangemethod. - Set the
valueproperty to the state'svideoProfileproperty. - Set the
widthto100%. - Loop through the video devices list using
this.state.videoDevices.map, setting the key and value properties toindexand 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
Microphonewrapped in a<label>element with the class namelabel. - Add a
<select>menu element wrapped in a set of nested<div>elements with the class namescontrolandselect.- Apply an
onChangeevent listener to invoke thethis.handleMicChangemethod. - Set the
valueproperty to the state'smicproperty. - Set the
widthto100%. - Loop through the audio devices list using
this.state.audioDevices.map, setting the key and value properties toindexand 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
Loudspeakerwrapped in a<label>element with the class namelabel. - Add a
<select>menu element wrapped in a set of nested<div>elements with the class namescontrolandselect.- Apply an
onChangeevent listener to invoke thethis.handleSpeakerChangemethod. - Set the
valueproperty to the state'sspeakerproperty. - Set the
widthto100%. - Loop through the audio playback devices list using
this.state.audioPlaybackDevices.map, setting the key and value properties toindexand 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
localproperty anddomare valid, set up the local video with thedomusingthis.props.rtcEngine.setupLocalVideo(). - If the
localproperty is invalid anddomis 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.