This tutorial shows you how to quickly create an open video call using the Agora sample app and the Agora 2.0+ API.
- Agora.io Developer Account
- Node.js 6.9.1+
- A web server that supports SSL (https)
This section shows you how to prepare, build, and run the sample application.
To build and run the sample application, get an App ID:
-
Create a developer account at agora.io.
-
In the Dashboard that opens, click Projects > Project List in the left navigation.
-
Copy the App ID from the Dashboard to a text file. You will use this when you launch the app.
-
Open the src/utils/Settings.js file. At the bottom of the file, replace
<#YOUR APP ID#>
with the App ID from the dashboard.Note: Place the App ID within single or double quotes.
export const APP_ID = <#YOUR APP ID#>;
-
Using the Terminal app, enter the
install
command in your project directory. This command installs libraries that are required to run the sample application.# install dependencies npm install
-
Start the application by entering the
run dev
orrun build
command.The
run dev
command is for development purposes.# serve with hot reload at localhost:8080 npm run dev
The
run build
command is for production purposes and minifies code.# build for production with minification npm run build
-
Your default browser should open and display the sample application.
Note: In some cases, you may need to open a browser and enter
http://localhost:8080
as the URL. -
Additional commands are available for the sample application.
Use the
run lint
command to use ESLint andrun format
command to improve code quality.# use eslint and prettier to improve code quality npm run lint npm run format
Use the
run test
command to run unit tests.# unit testing npm run test
The key code for the sample application is in the src
folder:
Folder name | Description |
---|---|
assets |
Contains stylesheets, fonts, and visual assets |
pages |
Contains layout UI code and JS code |
utils |
Contains helper JS classes and styles |
- Create Visual Assets
- Create the Index Page UI
- Create the Index JS Code
- Create the Pre-call Login Page UI
- Create the Pre-call Login JS Code
- Create the Meeting Page UI
- Create the Meeting JS Code
Add the following icon assets for the user interface to the src/assets/images
folder:
Asset | Description |
---|---|
ag-audience-active.png and ag-audience.png |
Images of a person to indicate if a user is active |
ag-browser.png |
Image used as an icon for browser testing |
ag-index-background.png |
Background image for the index page of the sample application |
ag-login.png |
Image of a monitor used to represent the channel |
ag-logo.png |
Agora logo |
ag-mic-active-s.png and ag-mic-s.png |
Images of a microphone to mute/unmute audio |
ag-oval-active.png and ag-oval.png |
Oval image used for radio dial selection |
ag-video-active-s.png and ag-video-s.png |
Images of a video camera to turn video on/off |
avatar.png |
Default avatar for a user |
The index page UI is contained in the src/pages/index/index.html
file.
The index page serves as the landing and login page for the sample application.
- Create the Page Architecture
- Create the Login Header and Footer
- Create the Channel Room Text Input
- Create the Login Type Menu
- Create the Login Mode Selectors
The main section of this page is within a <section>
element whose class is login-wrapper
.
Create the page footer
The main section of the login is within a <div>
element whose class is login-body
.
Create the header of the login area by adding:
- A reference to the Agora Logo,
../../assets/images/ag-logo.png
- The application title,
AgoraWeb v2.2
- The application subtitle application,
Powering Real-Time Communications
Create a page footer that contains a Join button that has joinBtn
as its id. This button logs the user into a room using details from by the login-body
area.
<div class="login-header">
<img src="../../assets/images/ag-logo.png" alt="">
<p class="login-title">AgoraWeb v2.2</p>
<p class="login-subtitle">Powering Real-Time Communications</p>
</div>
<div class="login-body">
...
</div>
<div class="login-footer">
<a id="joinBtn" class="ag-rounded button is-info">Join</a>
</div>
Create the first part of the login section:
- Add a text
<input>
element with the idchannel
for the room name of the channel. - Add an image reference to
ag-login.png
, within theinput
box. This image is for stylistic purposes only. - Add a
<span>
element whose class isvalidate-icon
. This element displays an icon if the room name validation has issues. - Add a
<span>
element whose class isvalidate-msg
. This element displays a message if the room name validation has issues.
<div class="columns">
<div class="column is-12">
<div id="channel-wrapper" class="control has-icons-left">
<input id="channel" class="ag-rounded input" type="text" placeholder="Input a room name here">
<span class="icon is-small is-left">
<img src="../../assets/images/ag-login.png" alt="">
</span>
<span class="validate-icon">
</span>
<div class="validate-msg"></div>
</div>
</div>
</div>
The Login type menu is comprised of two main sections:
- A basic login type menu within a
<div>
element whose ID isbaseModeDropdown
. - An advanced login type menu within a
<div>
element whose ID isadvanceProfileDropdown
.
<div id="dropdown-container">
<div class="dropdown" id="baseModeDropdown">
...
</div>
<div class="dropdown" id="advanceProfileDropdown">
...
</div>
</div>
The basic login type dropdown menu is comprised of:
- A menu selector within a
div
element whose class isdropdown-trigger
. - Menu options within a
div
element whose class isdropdown-menu
and whose ID isbaseModeOptions
.
<div class="dropdown-trigger">
...
</div>
<div class="dropdown-menu" id="baseModeOptions" role="menu">
...
</div>
Create a menu selector using an <a>
link element whose ID is basemode
and set the following properties.
Link Properties | Value | Description |
---|---|---|
data-value |
avc |
Determines the login type when logging in. |
class |
ag-rounded button |
Styles the menu. |
aria-haspopup |
true |
Indicates the menu selector has a popup menu to display. |
aria-controls |
baseModeOptions |
Indicates the ID of the popup menu to display. |
- Add the default selection option,
Agora Video Call
, using a<span>
element whose ID isbaseModeLabel
. This element updates when the selection option changes. - Add an arrow down icon using a
<span>
element whose class isicon is-small
. The<i>
element defines the icon within this<span>
element.
<a id="baseMode" data-value="avc" class="ag-rounded button" aria-haspopup="true" aria-controls="baseModeOptions">
<span id="baseModeLabel">Agora Video Call</span>
<span class="icon is-small">
<i class="ag-icon icon-arrow-down" aria-hidden="true"></i>
</span>
</a>
Create the menu options by placing the items within a set of nested <div>
elements. In the <div>
element whose class is dropdown-item
, add the available selection options within <p>
elements:
Agora Video Call
One to one and group calls
The <div>
element whose class is dropdown-item
has the following properties:
Property | Value | Description |
---|---|---|
data-value |
avc |
Determines the login type when logging in. |
data-msg |
Agora Video Call |
Replaces the text in the base menu selector. |
<div class="dropdown-content">
<div class="dropdown-item" data-value="avc" data-msg="Agora Video Call">
<p>Agora Video Call</p>
<hr>
<p>One to one and group calls</p>
</div>
</div>
The advanced login type dropdown menu is comprised of:
- The menu selector defined within the
div
element whose class isdropdown-trigger
. - The menu options defined within the
div
element whose class isdropdown-menu
and whose ID isadvancedOptions
.
<div class="dropdown-trigger">
...
</div>
<div class="dropdown-menu" id="advancedOptions" role="menu">
...
</div>
Create a menu selector using an <a>
link element whose ID is advancedProfile
, and set the following properties.
Link Properties | Value | Description |
---|---|---|
class |
ag-rounded button |
Styles the menu. |
aria-haspopup |
true |
Indicates the menu selector has a popup menu to display. |
aria-controls |
advancedOptions |
Indicates the ID of the popup menu to display. |
Add the default selection option, Advanced
, within a <span>
element.
<a id="advancedProfile" class="ag-rounded button" aria-haspopup="true" aria-controls="advancedOptions">
<span>Advanced</span>
</a>
Create the menu options by placing the items within a <div>
element whose class is dropdown-content
. Each of the three rows of the advanced menu is contained within a <div>
element whose class is dropdown-item
.
<div class="dropdown-content">
<div class="dropdown-item">
...
</div>
<div class="dropdown-item">
...
</div>
<div class="dropdown-item">
...
</div>
</div>
Create the Radio Options
Add a <div>
element whose class is control
. For each available selection option, add a <label>
element whose class is radio
. The <label>
element of each menu option contains:
- A radio
<input>
element namedtranscode
which is used as the selector. - A
<span>
element that describes the selection.
The available options are:
Input Value | Option Name | Description |
---|---|---|
interop |
VP8 & H264 |
Encoding with VP8 and decoding with H264 |
h264_interop |
H264-only |
Encoding and decoding with H264 |
<div class="control">
<label class="radio">
<input value="interop" type="radio" checked name="transcode">
<span>VP8 & H264</span>
</label>
<label class="radio">
<input value="h264_interop" type="radio" name="transcode">
<span>H264-only</span>
</label>
</div>
Create the High Stream Menu
Add a <span>
element with the text High Stream
for the dropdown menu label.
Wrap the <select>
menu element within a <div>
element whose class is select is-rounded
. The <select>
element has the classes ag-rounded
and is-clipped
and an ID of videoProfile
. The ID is used to populate the available high stream video profiles for the sample application.
<span>High Stream</span>
<div class="select is-rounded">
<select id="videoProfile" class="ag-rounded is-clipped">
</select>
</div>
Create the Low Stream Menu
Add a <span>
element with the text Low Stream
for the dropdown menu label.
Wrap the <select>
menu element within a <div>
element whose class is select is-rounded
. The <select>
element has the classes ag-rounded
and is-clipped
and an ID of videoProfileLow
. The ID is used to populate the available low stream video profiles for the sample application.
<span>Low Stream</span>
<div class="select is-rounded">
<select id="videoProfileLow" class="ag-rounded is-clipped">
</select>
</div>
Add a set of nested <div>
elements for the login mode selectors.
In the <div>
element whose ID is attendeeMode
and whose class is control
, add a <label>
element for each available selection option. The class of each <label>
element is radio
.
Each menu option <label>
element contains a radio <input>
element whose name is attendee
. Each <label>
element contains:
- A
<span>
element whose class isradio-btn
that styles the radio button. - A
<span>
element whose class isradio-img
that displays an icon for the radio option. - A
<span>
element whose class isradio-msg
that displays the radio option name.
The available radio options are:
Option Value | Option Name | Description |
---|---|---|
video |
Video Call : join with video call |
Joins the user with video capabilities. |
audio-only |
Audio-only : join with audio call |
Joins the user with only audio capabilities. |
audience |
Audience : join as an audience |
Joins the user as an audience member with no audio or video capabilities. |
<div class="columns">
<div class="column">
<div id="attendeeMode" class="control">
<label class="radio video">
<input value="video" type="radio" name="attendee" checked>
<span class="radio-btn">
</span>
<span class="radio-img video">
</span>
<span class="radio-msg">Video Call : join with video call</span>
</label>
<br>
<label class="radio audio-only">
<input value="audio-only" type="radio" name="attendee">
<span class="radio-btn">
</span>
<span class="radio-img audio">
</span>
<span class="radio-msg">Audio-only : join with audio call</span>
</label>
<br>
<label class="radio audience">
<input value="audience" type="radio" name="attendee">
<span class="radio-btn">
</span>
<span class="radio-img audience">
</span>
<span class="radio-msg">Audience : join as an audience</span>
</label>
</div>
</div>
</div>
The index functionality code is contained in the src/pages/index/index.js
file.
- Define the
getParameterByName
Constant - Define the
uiInit
Constant - Define the
validate
Constant - Define the
subscribeMouseEvent
Constant - Initialize the page
The getParameterByName
constant processes the parameters name
and url
and returns a string from the helper method decodeURIComponent()
.
- Check if the
url
is valid. If not, setwindow.location.href
as the value. - Modify
name
usingname.replace()
. - Use
name
to create a new regular expressionregex
usingnew RegExp()
. - Create a new local variable from
url
withregex.exec()
.
Return the result:
- If
results
is not valid, returnnull
. - If
results[2]
is not valid, return an empty string. - Otherwise, modify
results[2]
usingresults[2].replace()
and apply the resulting value to thedecodeURIComponent()
helper method return its result.
const getParameterByName = (name, url) => {
if (!url) {
url = window.location.href;
}
name = name.replace(/[[\]]/g, '\\$&');
let regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
let results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};
The uiInit
constant initializes the sample application.
Set the profileContainer
, profileLowContainer
, and lowResolutionArr
local variables.
Variable | Value | Description |
---|---|---|
profileContainer |
$('#videoProfile') |
Reference to the high stream video profile UI element. |
profileLowContainer |
$('#videoProfileLow') |
Reference to the low stream video profile UI element. |
lowResolutionArr |
Object.entries(RESOLUTION_ARR).slice(0,7) |
Array of available low resolution options. |
const uiInit = () => {
let profileContainer = $('#videoProfile');
let profileLowContainer = $('#videoProfileLow');
let lowResolutionArr = Object.entries(RESOLUTION_ARR).slice(0, 7);
...
};
Build the high stream video profile <options>
elements by mapping the items in Object.entries(RESOLUTION_ARR)
. The <option>
element properties are:
Property|Value|Description
selected
|selected
or empty|If item[0]
is equal to 480p_4
set the selected
property to set 480p as the default resolution.
value
|item[0]
|Sets the value property to the Agora resolution code.
Text label|item[1][0] + 'x' + item[1][1] + ', ' + item[1][2] + 'fps, ' + item[1][3] + 'kbps</option>'
|Sets the text label to display the aspect ratio and frame rate.
Append html
to profileContainer
using profileContainer.append()
and return its result.
Object.entries(RESOLUTION_ARR).map(item => {
let html =
'<option ' +
(item[0] === '480p_4' ? 'selected' : '') +
' value="' +
item[0] +
'">' +
item[1][0] +
'x' +
item[1][1] +
', ' +
item[1][2] +
'fps, ' +
item[1][3] +
'kbps</option>';
return profileContainer.append(html);
});
Build the low stream video profile <options>
elements by mapping the items in lowResolutionArr
. The <option>
element properties are:
Property | Value | Description |
---|---|---|
selected |
selected or empty |
If item[0] is equal to 180p_4 , set the selected property to 180p as the default resolution. |
value |
item[0] |
Sets the value property to the Agora resolution code. |
Text label | item[1][0] + 'x' + item[1][1] + ', ' + item[1][2] + 'fps, ' + item[1][3] + 'kbps</option>' |
Sets the text label to display the aspect ratio and frame rate. |
Append html
to profileContainer
using profileContainer.append()
and return its result.
lowResolutionArr.map(item => {
let html =
'<option ' +
(item[0] === '180p_4' ? 'selected' : '') +
' value="' +
item[0] +
'">' +
item[1][0] +
'x' +
item[1][1] +
', ' +
item[1][2] +
'fps, ' +
item[1][3] +
'kbps</option>';
return profileLowContainer.append(html);
});
Declare a new local variable, audienceOnly
. Set the value to true
if getParameterByName('audienceOnly')
is true. Otherwise, set the value to false
.
If audienceOnly
is true
:
- Hide the other attendance radio button options using
$('#attendeeMode label.audience').siblings().hide()
. - Select the audience radio button using
$('#attendeeMode label.audience input').prop('checked', true)
.
let audienceOnly = getParameterByName('audienceOnly') === 'true';
if (audienceOnly) {
$('#attendeeMode label.audience')
.siblings()
.hide();
$('#attendeeMode label.audience input').prop('checked', true);
}
The validate
constant processes the parameter channelName
and returns a string.
Use the helper class Validator
to determine what string value to return.
- Use
Validator.isNonEmpty()
to check ifchannelName
is empty. If true, return the stringCannot be empty!
. - Use
Validator.minLength()
to check ifchannelName
is less than 1 character. If true, return the stringNo shorter than 1!
. - Use
Validator.maxLength()
to check ifchannelName
is longer than 16 characters. If true, return the stringNo longer than 16!
. - Use
Validator.validChar()
to check ifchannelName
contains valid characters. If true, return the stringOnly capital or lower-case letter, number and "_" are permitted!
.
const validate = channelName => {
if (Validator.isNonEmpty(channelName)) {
return 'Cannot be empty!';
}
if (Validator.minLength(channelName, 1)) {
return 'No shorter than 1!';
}
if (Validator.maxLength(channelName, 16)) {
return 'No longer than 16!';
}
if (Validator.validChar(channelName)) {
return 'Only capital or lower-case letter, number and "_" are permitted!';
}
return '';
};
The subscribeMouseEvent
constant adds event listeners to the UI elements.
const subscribeMouseEvent = () => {
...
};
- Add the Join Button Event Listener and Callback
- Add the Window Keypress Event Listener and Callback
- Add the Channel Text Input Event Listener and Callback
- Add the Base Login Mode Dropdown Event Listeners and Callbacks
- Add the Advanced Options Event Listeners and Callbacks
- Add Dropdown Close Event Listener
Add a click
event listener to the Join UI button.
// Click Join and go to the meeting room
$('#joinBtn').on('click', () => {
...
});
- Validate the text in the
channel
name text input usingvalidate()
. - Retrieve the
validate-icon
UI element and clear out all its child nodes usingvalidateIcon.empty()
.
let validateRst = validate(
$('#channel')
.val()
.trim()
);
let validateIcon = $('.validate-icon');
validateIcon.empty();
If validateRst
is valid:
- Create a string
msg
for the error. - Apply the class
is-danger
to thechannel
text input box. - Append a validation icon
<i>
element whose class isicon-wrong
usingvalidateIcon.append()
. - Display the validation message in the validation message area using
$('.validate-msg').html(msg).show()
. - Return
0
.
Otherwise, continue processing the remaining code.
if (validateRst) {
let msg = `Input Error: ${validateRst}`;
$('#channel').addClass('is-danger');
validateIcon.append(`<i class="ag-icon icon-wrong"></i>`);
$('.validate-msg')
.html(msg)
.show();
return 0;
}
Apply the class is-success
to the channel
text input box and use validateIcon.append()
to append a validation icon <i>
element whose class is icon-correct
.
$('#channel').addClass('is-success');
validateIcon.append(`<i class="ag-icon icon-correct"></i>`);
Create a postData
object with the following values:
Name | Value | Description |
---|---|---|
baseMode |
document.querySelector('#baseMode').dataset.value |
Base mode value. |
transcode |
$('input[name="transcode"]:checked').val() |
Transcoding value. |
attendeeMode |
$('input[name="attendee"]:checked').val() |
Attendee mode value. |
videoProfile |
$('#videoProfile').val() |
High stream video profile value. |
channel |
$('#channel').val().trim() |
Channel name value. |
videoProfileLow |
$('#videoProfileLow').val() |
Low stream video profile value. |
Loop through the items in postData
using Object.entries(postData).map()
and set cookies for each item using Cookies.set()
.
let postData = {
baseMode: document.querySelector('#baseMode').dataset.value,
transcode: $('input[name="transcode"]:checked').val(),
attendeeMode: $('input[name="attendee"]:checked').val(),
videoProfile: $('#videoProfile').val(),
channel: $('#channel')
.val()
.trim(),
videoProfileLow: $('#videoProfileLow').val()
};
Object.entries(postData).map(item => {
return Cookies.set(item[0], item[1]);
});
Complete the callback by redirecting the user to the Pre-call page using window.location.href
.
window.location.href = 'precall.html';
Add an event listener for when the user presses Enter on their keyboard. The keyCode
for the Enter key is 13
. If the Enter key is pressed, trigger the Join button click
event listener using $('#joinBtn').trigger()
.
// Press Enter to trigger Join
window.addEventListener('keypress', e => {
e.keyCode === 13 && $('#joinBtn').trigger('click');
});
Add an event listener to validate the name for the channel
text input box while the user types.
// Add input check for room name
$('#channel').on('input', () => {
...
});
- Remove the classes
is-danger
andis-success
from thechannel
text input box. - Hide the validation UI message using
$('.validate-msg').hide()
. - Validate the text in the
channel
name text input usingvalidate()
. - Retrieve the
validate-icon
UI element and clear out all its child nodes usingvalidateIcon.empty()
.
$('#channel').removeClass('is-danger');
$('#channel').removeClass('is-success');
$('.validate-msg').hide();
let validateRst = validate(
$('#channel')
.val()
.trim()
);
let validateIcon = $('.validate-icon');
validateIcon.empty();
If validateRst
is valid:
- Create a string
msg
for the error. - Apply the class
is-danger
to thechannel
text input box. - Append a validation icon
<i>
element whose class isicon-wrong
usingvalidateIcon.append()
. - Display the validation message in the validation message area using
$('.validate-msg').html(msg).show()
. - Return
0
.
Otherwise, continue processing the remaining code.
if (validateRst) {
let msg = `Input Error: ${validateRst}`;
$('#channel').addClass('is-danger');
validateIcon.append(`<i class="ag-icon icon-wrong"></i>`);
$('.validate-msg')
.html(msg)
.show();
return 0;
}
Apply the class is-success
to the channel
text input box and use validateIcon.append()
to append a validation icon <i>
element whose class is icon-correct
.
$('#channel').addClass('is-success');
validateIcon.append(`<i class="ag-icon icon-correct"></i>`);
Add a click
event listener to the baseMode
UI element.
- Stop the bubbling of the
click
event to parent elements usinge.stopPropagation()
. - Remove the
is-active
class from all elements with thedropdown
class and toggle thebaseModeDropdown
element to theis-active
class.
// BaseMode dropdown control
$('#baseMode').click(e => {
e.stopPropagation();
$('.dropdown').removeClass('is-active');
$('#baseModeDropdown').toggleClass('is-active');
});
Add a click
event listener to the baseModeOptions
UI element.
- Stop the bubbling of the
click
event to parent elements usinge.stopPropagation()
. - Remove the
is-active
class from all elements with thedropdown
class and toggle thebaseModeDropdown
element to theis-active
class. - Apply the value for the selected option to the
baseMode
UI element using$('#baseMode').data()
. - Apply the text for the selected option to the
baseModeLabel
UI element using$('#baseModeLabel').html()
.
$('#baseModeOptions .dropdown-item').click(e => {
e.stopPropagation();
let [val, label] = [e.currentTarget.dataset.value, e.currentTarget.dataset.msg];
$('#baseMode').data('value', val);
$('#baseModeDropdown').removeClass('is-active');
$('#baseModeLabel').html(label);
});
Add a click
event listener to the advancedProfile
UI element.
- Stop the bubbling of the
click
event to parent elements usinge.stopPropagation()
. - Remove the
is-active
class from all elements with thedropdown
class and toggle theadvanceProfileDropdown
element to theis-active
class.
// AdvancedProfile dropdown control
$('#advancedProfile').click(e => {
e.stopPropagation();
$('.dropdown').removeClass('is-active');
$('#advanceProfileDropdown').toggleClass('is-active');
});
Add a click
event listener to the advancedOptions
UI element.
Stop the bubbling of the click
event to parent elements using e.stopPropagation()
.
$('#advancedOptions').click(e => {
e.stopPropagation();
});
Add a click
event listener to the window
using $(window).click()
.
Close any open dropdown menus by removing the is-active
class using $('.dropdown').removeClass()
.
// global click will close dropdown
$(window).click(_ => {
$('.dropdown').removeClass('is-active');
});
To initialize the page, use uiInit()
to initialize the UI elements and use subscribeMouseEvent()
to add event listeners and callbacks to the UI elements.
uiInit();
subscribeMouseEvent();
The pre-call login page UI is contained in the src/pages/precall/precall.html
file.
Logging in displays a pre-call page where the user can test their audio and video before joining the call.
The main section of this page is within the <section>
element whose class is login-wrapper
.
Create the page footer:
- Add the text
Powered By Agora
linking to the Agora website. - Add a reference to Agora's sales support email.
The main section of this page is within the <div>
element whose class is ag-container
.
Create the page header by adding a reference to the Agora Logo, ../../assets/images/ag-logo.png
, and the application title, AgoraWeb v2.2
.
Create the page footer:
- Add the text
Powered By Agora
linking to the Agora website. - Add a reference to Agora's support phone number
400 632 6626
.
Include the Agora SDK file from the source https://cdn.agora.io/sdk/web/AgoraRTCSDK-2.5.0.js
.
The left column displays the setting details for the room. Create the following <p>
elements for each setting. These UI elements are used to display the settings from the index page.
Label | <span> ID |
Description |
---|---|---|
Room Name: |
channel |
Room name for the channel |
Base Mode: |
baseMode |
Base mode |
Attendee Mode: |
attendeeMode |
Attendee mode |
Video Profile: |
videoProfile |
Video profile (resolution and frame rate) |
Transcode: |
transcode |
Transcoding method |
At the bottom of the left column, add a Quick Join
UI button with the ID quickJoinBtn
. This button will log the user into the channel room, and load the meeting area.
<div class="column">
<div class="ag-info">
<p>Room Name:
<span id="channel"></span>
</p>
<p>Base Mode:
<span id="baseMode"></span>
</p>
<p>Attendee Mode:
<span id="attendeeMode"></span>
</p>
<p>Video Profile:
<span id="videoProfile"></span>
</p>
<p>Transcode:
<span id="transcode"></span>
</p>
<div class="ag-info-footer">
<a id="quickJoinBtn" class="ag-rounded button">
<span>Quick Join</span>
</a>
</div>
</div>
</div>
The right column allows for device testing before entering the channel room.
- Create the Device Testing Architecture
- Create the Video Testing Section
- Create the Audio Testing Section
- Create the Loss Measurement Section
Create the step header in a <div>
element whose class is ag-steps
.
Add two steps as <div>
elements whose class is step
and the IDs stepOne
and stepTwo
. Highlight step 1 by including the class active
on its <div>
element.
The video, audio, and loss measurement testing sections are contained within <section>
elements whose class is ag-card
and IDs videoCard
, audioCard
, and connectCard
.
<div class="column">
<div class="ag-cards">
<section class="ag-cards-title">
<div class="ag-steps">
<div id="stepOne" class="step active">1</div>
<div id="stepTwo" class="step">2</div>
</div>
</section>
<section class="ag-card" id="videoCard">
...
</section>
<section class="ag-card" id="audioCard">
...
</section>
<section class="ag-card" id="connectCard">
...
</section>
</div>
</div>
Create the header by adding <span>
elements to the <div>
element whose class is ag-card-header
:
- In the first
<span>
element, add an<i>
element whose classes areag-icon
andag-icon-video-24
>. This element displays the video icon. - In the second
<span>
element, add the textVideo
to label the section.
Add a <div>
element whose class is ag-card-tip
to provide instructions on how to conduct the video camera test.
<div class="ag-card-header">
<span>
<i class="ag-icon ag-icon-video-24"></i>
</span>
<span>Video</span>
</div>
<div class="ag-card-tip">
Move in front of the camera to check if it works.
</div>
Create the main video device testing area by adding two <div>
classes to the <div>
element whose class is ag-card-body
.
- The
<div>
whose class isinitial
contains the UI elements for the video device testing controls. - The
<div>
whose class isresult
contains the UI element that will display the testing video results.
In the <div>
whose class is initial
, add a select dropdown menu whose classes are is-clipped
and ag-rounded
and whose ID is videoDevice
. This element is nested in another <div>
element whose classes are select
and ag-select
and whose ID is videoDevice
. This <select>
element will populate with the available video devices.
In the <div>
whose class is ag-video-test
, add a <div>
element whose class is ag-video-test
that contains the <div>
elements with IDs videoItem
and enableVideoSwitch
.
-
The
videoItem
element will display the video preview for testing. -
The
enableVideoSwitch
contains a checkbox<input>
element with the ID and nameenableVideo
and a<label>
element.Note: The checkbox element applies the classes
switch
andis-rounded
to display the control as a switch rather than a checkbox. It is initialized as "on" with the classis-success
.
<div class="ag-card-body">
<div class="initial">
<div class="select ag-select">
<select class="is-clipped ag-rounded" id="videoDevice">
</select>
</div>
<div class="ag-video-test">
<div id="videoItem">
</div>
<div class="field" id="enableVideoSwitch">
<input id="enableVideo" type="checkbox" name="enableVideo" class="switch is-rounded is-success">
<label for="enableVideo"></label>
</div>
</div>
</div>
<div class="result"></div>
</div>
Create the header by adding <span>
classes to the <div>
element whose class is ag-card-header
:
- In the first
<span>
element, add an<i>
element whose classes areag-icon
andag-icon-audio-24
>. This element displays the audio icon. - In the second
<span>
element, add the textAudio
to label the section.
Add a <div>
element whose class is ag-card-tip
to provide instructions on how to conduct the video camera test.
<div class="ag-card-header">
<span>
<i class="ag-icon ag-icon-audio-24"></i>
</span>
<span>Audio</span>
</div>
<div class="ag-card-tip">
Produce sounds to check if the mic works.
</div>
Create the main audio device testing area by adding two <div>
classes to the <div>
element whose class is ag-card-body
.
- The
<div>
whose class isinitial
contains the UI elements for the video device testing controls. - The
<div>
whose class isresult
contains the UI element that will display the testing video results.
In the <div>
whose class is initial
, add a select dropdown menu whose classes are is-clipped
and ag-rounded
and whose ID is audioDevice
. This element is nested in another <div>
element whose classes are select
and ag-select
and whose ID is audioDevice
. This <select>
element will populate with the available audio devices.
In the <div>
whose class is ag-audio-test
:
- Add an
<i>
element whose classes areag-icon
andag-icon-audio-24
, nested within a<span>
element. This displays an audio icon. - Add a
<progress>
element with the IDvolume
and classesprogress
,is-small
, andis-info
. This serves as the volume indicator for the audio device and is initialized to a value of0
and amax
value limit of100
.
<div class="ag-card-body">
<div class="initial">
<div class="select ag-select">
<select class="is-clipped ag-rounded" id="audioDevice">
</select>
</div>
<div class="ag-audio-test">
<span>
<i class="ag-icon ag-icon-audio-24"></i>
</span>
<progress id="volume" class="progress is-small is-info" value="0" max="100"></progress>
</div>
</div>
<div class="result"></div>
</div>
Create the header by adding <span>
classes to the <div>
element whose class is ag-card-header
:
- In the first
<span>
element, add an<i>
element whose classes areag-icon
andag-icon-connect-24
> which is used to display the connection icon. - In the second
<span>
element, add the textLoss Measurement
to label the section.
<div class="ag-card-header">
<span>
<i class="ag-icon ag-icon-connect-24"></i>
</span>
<span>Loss Measurement</span>
</div>
Create the main lost measurement testing area by adding a <div>
whose class is ag-browser-test
to the <div>
element whose classes are ag-card-body
and ag-connect-test
.
In the <div>
whose class is ag-browser-test
:
- Add an
<img>
element that displays theag-browser.png
image asset. - Add a
<p>
element with the IDcompatibility
. This element will display the compatibility with the Agora SDK.
<div class="ag-card-body ag-connect-test">
<div class="ag-browser-test">
<img src="../../assets/images/ag-browser.png" alt="">
<p>Browser Compatibility:
<span id="compatibility"></span>
</p>
</div>
</div>
The settings functionality code is contained in the src/pages/precall/precall.js
file.
- [Define Global Variables](#define-global variables)
- Define the
uiInit
Constant - Define the
Schedule
Constant - Define the
clientInit
Constant - Define the
receiverInit
Constant - Define the
setDevice
Constant - Define the
subscribeEvents
Constant - [Initialize the Page](#initialize-the page)
Define the global variables for the pre-call page.
Variable | Description |
---|---|
stream |
The channel stream |
recvStream |
The received channel stream |
client |
The Agora client |
receiver |
The receiver for the client |
key |
The Agora App ID key |
_testChannel |
The channel name for testing. The value is set to a random number so users can test without conflicting with live channel rooms. |
let stream;
let recvStream;
let client;
let receiver;
let key;
let _testChannel = String(
Number.parseInt(new Date().getTime(), 10) + Math.floor(Math.random() * 1000)
);
The uiInit
constant is used to initialize the pre-call page of the sample application.
Set the profile
and transcodeValue
local variables based on the browser cookies using Cookies.get()
.
profile
is the retrieved cookie index ofRESOLUTION_ARR
.- If the
transcode
cookie is not valid, thetranscodeValue
isinterop
.
The remaining code for this section is contained within the Promise
return.
// Init ui
const uiInit = () => {
return new Promise((resolve, reject) => {
// Init info card
let profile = RESOLUTION_ARR[Cookies.get('videoProfile')];
let transcodeValue = Cookies.get('transcode') || 'interop';
...
})
};
Create a transcode
variable which returns a string based on transcodeValue
.
transcodeValue Value |
transcode Value |
Description |
---|---|---|
Empty string | VP8-only |
Encoding and decoding with VP8 |
interop |
VP8 & H264 |
Encoding with VP8 and decoding with H264 |
h264_interop |
H264-only |
Encoding and decoding with H264 options |
let transcode = (() => {
switch (transcodeValue) {
case '':
return 'VP8-only';
default:
case 'interop':
return 'VP8 & H264';
case 'h264_interop':
return 'H264-only';
}
})();
Create an info
object with the following information.
Name | Value | Description |
---|---|---|
videoProfile |
${profile[0]}x${profile[1]} ${profile[2]}fps ${profile[3]}kbps |
High stream video profile value. |
channel |
`Cookies.get('channel') | |
transcode |
Empty value | Transcoding value. |
attendeeMode |
`Cookies.get('attendeeMode') | |
baseMode |
`Cookies.get('baseMode') |
let info = {
videoProfile: `${profile[0]}x${profile[1]} ${profile[2]}fps ${profile[3]}kbps`,
channel: Cookies.get('channel') || 'test',
transcode,
attendeeMode: Cookies.get('attendeeMode') || 'video',
baseMode: Cookies.get('baseMode') || 'avc'
};
If the base mode is avc
, set key
to APP_ID
.
// Init key
if (info.baseMode === 'avc') {
key = APP_ID;
}
Loop through the info
object and update its associated UI element using $('#' + item[0]).html()
.
If info.attendeeMode
is equal to video
, check the enableVideo
UI element using $('#enableVideo').prop()
.
Object.entries(info).map(item => {
// Find dom and insert info
return $('#' + item[0]).html(item[1]);
});
// Video-attendee's switch
if (info.attendeeMode === 'video') {
$('#enableVideo').prop('checked', true);
}
Check the system requirements for the Agora SDK.
If the system requirements are valid, set the compatibility
element text to AgoraRTC supported.
using $('#compatibility').html()
. Otherwise, set the text to AgoraRTC not fully supported and some functions may be lost.
// Init compatibility result
// eslint-disable-next-line
if (AgoraRTC.checkSystemRequirements()) {
$('#compatibility').html('AgoraRTC supported.');
} else {
$('#compatibility').html(
'AgoraRTC not fully supported and some functions may be lost.'
);
}
Initialize the device options based on the browser used using isSafari()
.
-
If the browser is Safari, remove the default audio devices using
$('#audioDevice').parent().remove()
, remove any video devices using$('#videoDevice').parent().remove()
, and invoke theresolve()
method. -
If another browser is used, populate the
videoDevice
andaudioDevice
UI elements using$('#videoDevice').html()
and$('#audioDevice').html()
, and invoke theresolve()
method.Note: Retrieve the HTML for the video and audio options by looping through the
item
object and appending<option>
elements whose values are composed ofitem.deviceId
anditem.label
. Audio devices are defined by theitem.kind
valueaudioinput
. Video devices are defined by theitem.kind
valuevideoinput
.
// Init device options
if (isSafari()) {
// If safari, disable set device since deviceId changes all the time
$('#audioDevice')
.parent()
.remove();
$('#videoDevice')
.parent()
.remove();
resolve()
} else {
// eslint-disable-next-line
AgoraRTC.getDevices(function(devices) {
let videoHtml = '';
let audioHtml = '';
devices.forEach(function(item) {
if (item.kind === 'audioinput') {
audioHtml += '<option value=' + item.deviceId + '>' + item.label + '</option>';
}
if (item.kind === 'videoinput') {
videoHtml += '<option value=' + item.deviceId + '>' + item.label + '</option>';
}
});
$('#videoDevice').html(videoHtml);
$('#audioDevice').html(audioHtml);
resolve()
});
}
The Schedule
constant is used to check for changes in the device testing results.
Set the following object parameters:
Parameter | Value | Description |
---|---|---|
DURATION |
10 |
Number of seconds for refresh |
volume |
0 |
Volume |
volumeBar |
$('#volume') |
Volume UI element |
targetStream |
Empty object | Target stream object |
getVolume(stream) |
See getVolume() Method |
|
scheduleVolumeDetect |
Empty object | Volume detection schedule information |
scheduleEnd |
Empty object | End schedule information |
start() |
See start() Method |
|
reset() |
See reset() Method |
|
init(stream, duration) |
See init() Method |
const Schedule = {
DURATION: 10,
volume: 0,
volumeBar: $('#volume'),
targetStream: {},
getVolume(stream) {
...
},
scheduleVolumeDetect: {},
scheduleEnd: {},
start() {
...
},
reset() {
...
},
init(stream, duration) {
...
}
};
The getVolume()
method retrieves the stream's audio level using stream.getAudioLevel()
and returns its value times 100
. If vol
is not valid, return 0
.
Note: The volume value is rounded to the nearest integer using Math.round
for easier UI display.
let vol = Math.round(stream.getAudioLevel() * 100);
if (isNaN(vol)) {
return 0;
}
return vol;
The start()
checks if the target stream is valid before executing the remaining code. If this.targetStream
is not valid, log an error in the console using console.error()
.
let that = this;
if (!this.targetStream) {
console.error('Please init Schedule with a targetStream!');
return;
}
Prepend a <div>
element with the ID testDuration
to ag-connect-test
UI elements.
Initialize the volume detector with a time interval of 100
milliseconds. The volume detector retrieves the volume of that.targetStream
using that.getVolume()
and updates the volume bar UI value using that.volumeBar.val()
.
$('.ag-connect-test').prepend('<div id="testDuration"></div>');
// Init volume detector
this.scheduleVolumeDetect = setInterval(function() {
that.volume = that.getVolume(that.targetStream);
that.volumeBar.val(that.volume);
}, 100);
Initialize the schedule end detector with a time interval of this.DURATION
seconds. The duration is multiplied by 1000
because the method requires time to be set in milliseconds.
The schedule end detector:
- Sets the
style
attribute of thetestDuration
UI element toanimation-play-state:paused;background-color:#7ED321
using$('#testDuration').attr()
. - Clears the schedule volume detector using
clearInterval()
. - Invokes
that.targetStream.getStats()
. - Updates the step selection to step 2 using
$('#stepTwo').addClass()
and$('#stepOne').removeClass()
.
The remaining code in this section pertains to the callback for that.targetStream.getStats()
.
// Init timer for detect
this.scheduleEnd = setTimeout(function() {
$('#testDuration').attr(
'style',
'animation-play-state:paused;background-color:#7ED321'
);
clearInterval(that.scheduleVolumeDetect);
that.targetStream.getStats(function(e) {
...
});
// Update to step 2
$('#stepTwo').addClass('active');
$('#stepOne').removeClass('active');
}, this.DURATION * 1000);
Create an array with the following testing information:
Variable | Value | Description |
---|---|---|
videoBytes |
e.videoReceiveBytes |
Number of video bytes received |
audioBytes |
e.audioReceiveBytes |
Number of audio bytes received |
videoPackets |
e.videoReceivePackets |
Number of video packets received |
audioPackets |
e.audioReceivePackets |
Number of audio packets received |
videoPacketsLost |
e.videoReceivePacketsLost |
Number of video packets lost |
audioPacketsLost |
e.audioReceivePacketsLost |
Number of audio packets lost |
let [
videoBytes,
audioBytes,
videoPackets,
audioPackets,
videoPacketsLost,
audioPacketsLost
] = [
e.videoReceiveBytes,
e.audioReceiveBytes,
e.videoReceivePackets,
e.audioReceivePackets,
e.videoReceivePacketsLost,
e.audioReceivePacketsLost
];
Define the following local variables:
Variable | Value | Description |
---|---|---|
videoBitrate |
(videoBytes / 1000 / that.DURATION).toFixed(2) + 'KB/s' |
Video bit rate calculation |
audioBitrate |
(audioBytes / 1000 / that.DURATION).toFixed(2) + 'KB/s' |
Audio bit rate calculation |
vPacketLoss |
(videoPacketsLost / videoPackets * 100).toFixed(2) + '%' |
Video packet loss percentage calculation |
aPacketLoss |
(audioPacketsLost / audioPackets * 100).toFixed(2) + '%' |
Audio packet loss percentage calculation |
sumPacketLoss |
(videoPacketsLost / videoPackets * 100 + audioPacketsLost / audioPackets * 100).toFixed(2) |
Total packet loss percentage calculation |
// Do calculate
let videoBitrate = (videoBytes / 1000 / that.DURATION).toFixed(2) + 'KB/s';
let audioBitrate = (audioBytes / 1000 / that.DURATION).toFixed(2) + 'KB/s';
let vPacketLoss = (videoPacketsLost / videoPackets * 100).toFixed(2) + '%';
let aPacketLoss = (audioPacketsLost / audioPackets * 100).toFixed(2) + '%';
let sumPacketLoss = (
videoPacketsLost / videoPackets * 100 +
audioPacketsLost / audioPackets * 100
).toFixed(2);
Render the result of the bitrate and loss to the UI.
Set the videoCard
and audioCard
variables to reference those UI elements quickly.
Create videoCardHtml
and audioCardHtml
local variables, applying a <div>
element whose class is ag-test-result
. In the <div>
element, add two <p>
elements for the bit rate values ${videoBitrate}
and ${audioBitrate}
and the packet loss values ${vPacketLoss}
and ${aPacketLoss}
.
// Render result
let videoCard = $('#videoCard .ag-card-body');
let audioCard = $('#audioCard .ag-card-body');
let videoCardHtml = `
<div class="ag-test-result">
<p>Video Bitrate: ${videoBitrate}</p>
<p>Packet Loss: ${vPacketLoss}</p>
</div>
`;
let audioCardHtml = `
<div class="ag-test-result">
<p>Audio Bitrate: ${audioBitrate}</p>
<p>Packet Loss: ${aPacketLoss}</p>
</div>
`;
Set qualityHtml
based on the value of sumPacketLoss
:
sumPacketLoss Value |
qualityHtml Value |
---|---|
< 1 |
Excellent |
< 5 |
Good |
< 10 |
Poor |
< 100 |
Bad |
Any other value | Get media failed. |
let qualityHtml;
if (sumPacketLoss < 1) {
qualityHtml = 'Excellent';
} else if (sumPacketLoss < 5) {
qualityHtml = 'Good';
} else if (sumPacketLoss < 10) {
qualityHtml = 'Poor';
} else if (sumPacketLoss < 100) {
qualityHtml = 'Bad';
} else {
qualityHtml = 'Get media failed.';
}
Update the child elements for videoCard
and audioCard
:
- Hide all elements whose class is
initial
usinghide()
. - Update the inner HTML for all elements with the class
result
usinghtml()
withvideoCardHtml
andaudioCardHtml
.
Use empty()
to remove the child nodes of the testDuration
element. Use after()
to append the qualityHtml
value within a <span>
element.
videoCard.find('.initial').hide();
videoCard.find('.result').html(videoCardHtml);
audioCard.find('.initial').hide();
audioCard.find('.result').html(audioCardHtml);
$('#testDuration')
.empty()
.after(`<span style="">${qualityHtml}</span>`);
The reset()
method resets the test settings and UI.
- Remove the
testDuration
UI element usingremove()
. - Set
volume
to0
. - Set the value of the volume bar to
0
usingthis.volumeBar.val()
. - If
this.scheduleVolumeDetect
exists, clear the interval usingclearInterval()
. - If
this.scheduleEnd
exists, clear the timeout usingclearTimeout()
. - Set
this.targetStream
tonull
.
$('#testDuration').remove();
this.volume = 0;
this.volumeBar.val(0);
if (this.scheduleVolumeDetect) {
clearInterval(this.scheduleVolumeDetect);
}
if (this.scheduleEnd) {
clearTimeout(this.scheduleEnd);
}
this.targetStream = null;
The init()
method initializes the object parameters.
- Set
this.targetStream
tostream
. - Set
this.DURATION
toduration
.
this.targetStream = stream;
this.DURATION = duration;
The clientInit()
constant is used to initialize the Agora client for the sample application.
Create the Agora client using AgoraRTC.createClient()
. The transcode
mode is retrieved from the browser cookies using Cookies.get()
. If the transcode
is not valid, use interop
.
Initialize the Agora client using client.init()
and invoke client.join()
. The remaining code in this section pertains to the client.join()
callback.
// Init client
const clientInit = () => {
return new Promise((resolve, reject) => {
// eslint-disable-next-line
client = AgoraRTC.createClient({
mode: Cookies.get('transcode') || 'interop'
});
client.init(key, () => {
client.join(key, _testChannel, undefined, uid => {
...
});
});
});
};
Create a defaultConfig
object with the following information:
Config Name | Value | Description |
---|---|---|
streamID |
uid |
Stream ID. Uses the User ID as default. |
audio |
true |
Enable audio indicator. |
video |
true |
Enable video indicator. |
screen |
false |
Share screen indicator. |
cameraId |
$('#videoDevice').val() |
Device ID for video. |
microphoneId |
$('#audioDevice').val() |
Device ID for audio. |
// Init stream
let defaultConfig = {
streamID: uid,
audio: true,
video: true,
screen: false,
cameraId: $('#videoDevice').val(),
microphoneId: $('#audioDevice').val()
};
// eslint-disable-next-line
stream = AgoraRTC.createStream(defaultConfig);
Set the video profile from the videoProfile
browser cookie using Cookies.get()
. If the videoProfile
is invalid, use 480p_4
.
Initialize the stream using stream.init()
.
If successful:
- Use
client.publish()
to publish the stream. - If the
enableVideo
UI element is not checked, disable the video usingstream.disableVideo()
. - Invoke the
resolve
method.
If the stream initialization has an error, log it into the console using console.log()
and invoke reject()
.
stream.setVideoProfile(Cookies.get('videoProfile').split(',')[0] || '480p_4');
stream.init(
() => {
client.publish(stream);
if (!$('#enableVideo').prop('checked')) {
stream.disableVideo();
}
resolve();
},
err => {
console.log('getUserMedia failed', err);
reject(err);
}
);
The receiverInit()
constant is used to initialize the Agora client
.
To create the receiver, create an Agora client object using AgoraRTC.createClient()
. For the mode
value, use Cookies.get()
to retrieve the transcode
value from the browser cookies. If the transcode
cookie is invalid, use interop
.
The remaining code for this section is contained within the Promise
callback, which initializes the and adds stream event listeners.
// Init receiver
const receiverInit = () => {
return new Promise((resolve, reject) => {
// eslint-disable-next-line
receiver = AgoraRTC.createClient({
mode: Cookies.get('transcode') || 'interop'
});
...
});
};
Add the stream-added
event listener.
This event is triggered when a stream is added and subscribes the stream to the client using receiver.subscribe()
. If subscribing the stream fails, log the error to the console using console.log()
.
receiver.on('stream-added', function(evt) {
let stream = evt.stream;
receiver.subscribe(stream, function(err) {
console.log('Subscribe stream failed', err);
});
});
Add the stream-subscribed
event listener. This event is triggered when a stream is subscribed to the client.
- If
recvStream
exists, invokerecvStream.stop()
. - Reset the Schedule helper class using
Schedule.reset()
. - Set
recvStream
toevt.stream
. - Use
Schedule.init()
to initialize the Schedule helper class withrecvStream
and an interval of10
. - Play the stream in the
videoItem
UI element usingrecvStream.play()
. - Start the Schedule helper class using
Schedule.start()
.
receiver.on('stream-subscribed', function(evt) {
if (recvStream) {
recvStream.stop();
}
Schedule.reset();
recvStream = evt.stream;
Schedule.init(recvStream, 10);
recvStream.play('videoItem');
Schedule.start();
});
Add the peer-leave
event listener. This event is triggered when a peer leaves and empties the child nodes of the videoItem
UI element using empty()
.
receiver.on('peer-leave', function(_) {
$('#videoItem').empty();
});
Add the stream-removed
event listener. This event is triggered when a stream is removed and uses empty()
to remove the child nodes of the videoItem
UI element.
receiver.on('stream-removed', function(_) {
$('#videoItem').empty();
});
Initialize the client using receiver.init()
, which invokes receiver.join()
. The parameters for the join()
method are:
Parameter | Description |
---|---|
key |
Agora App ID |
_testChannel |
Channel name for the device tests |
undefined |
Unique channel name for the session (not needed) |
uid |
User ID callback invokes resolve() |
err |
Error callback invokes reject() |
receiver.init(key, () => {
receiver.join(
key,
_testChannel,
undefined,
uid => {
resolve(uid);
},
err => {
reject(err);
}
);
});
The setDevice
constant is used to initialize the devices for the sample app.
Ensure stream
is valid or throw an Error
.
The remaining code in this section is contained within the Promise()
callback.
// Set Device
const setDevice = () => {
if (!stream) {
throw Error('Stream not existed!');
}
return new Promise((resolve, reject) => {
...
});
};
- Set the stream
id
using the valuestream.getId()
. - Unpublish the
stream
from the client usingclient.unpublish()
. - Stop the
stream
usingstop()
. - Close the
stream
usingclose()
.
let id = stream.getId();
client.unpublish(stream);
stream.stop();
stream.close();
Create the defaultConfig
object and apply it to create the stream using AgoraRTC.createStream()
. The configuration properties are:
Config Name | Value | Description |
---|---|---|
streamID |
id |
Stream ID |
audio |
true |
Enable audio indicator |
video |
true |
Enable video indicator |
screen |
false |
Share screen indicator |
cameraId |
$('#videoDevice').val() |
Device ID for video |
microphoneId |
$('#audioDevice').val() |
Device ID for audio |
// Reinit stream
let defaultConfig = {
streamID: id,
audio: true,
video: true,
screen: false,
cameraId: $('#videoDevice').val(),
microphoneId: $('#audioDevice').val()
};
// eslint-disable-next-line
stream = AgoraRTC.createStream(defaultConfig);
Use stream.setVideoProfile()
to set the video profile with the videoProfile
saved in the browser cookies. If the videoProfile
browser cookie is invalid, use 480p_4
.
Initialize the stream using stream.init()
.
If successful:
- If the
enableVideo
UI element is not checked, disable video usingstream.disableVideo()
. - Publish the stream using
client.publish()
. - Invoke the
resolve
method.
If the stream initialization has an error, log it into the console using console.log()
and invoke reject()
.
stream.setVideoProfile(Cookies.get('videoProfile').split(',')[0] || '480p_4');
stream.init(
() => {
if (!$('#enableVideo').prop('checked')) {
stream.disableVideo();
}
client.publish(stream);
resolve();
},
err => {
console.log('getUserMedia failed', err);
reject(err);
}
);
The subscribeEvents
constant is used to add event listeners to the UI elements.
// Subscribe events
const subscribeEvents = () => {
...
};
- Add the Join Click Event Listener and Callback
- Add the Video Device Change Event Listener and Callback
- Add the Audio Device Change Event Listener and Callback
- Add the Enable Video Change Event Listener and Callback
Add a click
event listener to the quickJoinBtn
UI element.
Set the cameraId
and microphoneId
browser cookie values using Cookies.set()
with $('#videoDevice').val()
and $('#audioDevice').val()
.
Execute the following before redirecting to the meeting page using window.location.href
:
- If
client
exists and unpublishing the stream usingclient.unpublish(stream)
is successful. - If
stream
exists and closing the stream usingstream.close()
is successful. - If
client
exists and leaving the client usingclient.leave()
is successful.
$('#quickJoinBtn').on('click', function() {
Cookies.set('cameraId', $('#videoDevice').val());
Cookies.set('microphoneId', $('#audioDevice').val());
try {
client && client.unpublish(stream);
stream && stream.close();
client &&
client.leave(
() => {
console.log('Client succeed to leave.');
},
() => {
console.log('Client failed to leave.');
}
);
} finally {
// Redirect to index
window.location.href = 'meeting.html';
}
});
Add a change
event listener to the videoDevice
UI element. The listener uses Schedule.reset()
to reset the Schedule helper class and uses setDevice()
to set the device list.
$('#videoDevice').change(function(_) {
Schedule.reset();
setDevice();
});
Add a change
event listener to the audioDevice
UI element. The listener uses Schedule.reset()
to reset the Schedule helper class and uses setDevice()
to set the device list.
$('#audioDevice').change(function(_) {
Schedule.reset();
setDevice();
});
Add a change
event listener to the enableVideo
UI element.
- If the
enableVideo
UI element is checked,stream.enableVideo()
enables the stream's video. - If the
enableVideo
UI element is not checked,stream.disableVideo()
disables the stream's video.
$('#enableVideo').change(function(_) {
if ($('#enableVideo').prop('checked')) {
stream.enableVideo();
} else {
stream.disableVideo();
}
});
Initialize the UI using uiInit()
. When initialization completes:
- Subscribe the event listeners using
subscribeEvents()
, - Initialize the client using
clientInit()
, - Initialize the receiver using
receiverInit()
,
// --------------- start ----------------
uiInit().then(() => {
subscribeEvents();
clientInit();
receiverInit();
});
The meeting page UI is contained in the src/pages/meeting/meeting.html
file.
The user enters the meeting page after logging in from the pre-call login page. This page serves as the audio/video call page for the sample application.
The <div>
element with the ID ag-canvas
is where the video will display and contains a list of button controls for the sample application. The controls for this page is within the <div>
element whose class is ag-btn-group
.
Create the page header:
- Add a reference to the Agora Logo,
../../assets/images/ag-logo.png
, and the application title,AgoraWeb v2.2
. - Add a placeholder for the room name text within a
<span>
element of idroom-name
. This element will be used to display the actual room name after the user logs in from the pre-call page.
Create the page footer page:
- Add the text
Powered By Agora
linking to the Agora website. - Add a reference to Agora's support phone number.
Add a JavaScript reference to the Agora SDK https://cdn.agora.io/sdk/web/AgoraRTCSDK-2.3.1.js
.
<div class="wrapper" id="page-meeting">
<div class="ag-header">
<div class="ag-header-lead">
<img class="ag-header-logo" src="../../assets/images/ag-logo.png" alt="">
<span>AgoraWeb v2.2</span>
</div>
<div class="ag-header-msg">
Room:
<span id="room-name">--</span>
</div>
</div>
<div class="ag-main">
<div class="ag-container" id="ag-canvas">
<!-- btn group -->
<div class="ag-btn-group">
...
</div>
</div>
</div>
<div class="ag-footer">
<a class="ag-href" target="_blank" href="https://www.agora.io">
<span>Powered By Agora</span>
</a>
<span>Talk to Support: 400 632 6626</span>
</div>
<!-- modal/messages/notifications -->
</div>
<script src="https://cdn.agora.io/sdk/web/AgoraRTCSDK-2.3.1.js"></script>
<!-- inject -->
Add a set of controls for the meeting page, specified by <i>
elements wrapped within a <span>
element:
Button <span> Class |
Title | <i> Class |
Description |
---|---|---|---|
exitBtn |
Exit |
icon-call-ends |
Exit button |
videoControlBtn |
Enable/Disable Video |
icon-camera and icon-camera-off |
Enable/Disable Video button. |
audioControlBtn |
Enable/Disable Audio |
icon-mic and icon-mic-off |
Enable/Disable Audio button. |
shareScreenBtn |
Share Screen |
icon-screen-share |
Share screen button. |
displayModeBtn |
Switch Display Mode |
icon-switch-layout |
Switch display mode button. |
<span class="ag-btn exitBtn" title="Exit">
<i class="ag-icon icon-call-ends"></i>
</span>
<span class="ag-btn videoControlBtn" title="Enable/Disable Video">
<i class="ag-icon icon-camera"></i>
<i class="ag-icon icon-camera-off"></i>
</span>
<span class="ag-btn audioControlBtn" title="Enable/Disable Audio">
<i class="ag-icon icon-mic"></i>
<i class="ag-icon icon-mic-off"></i>
</span>
<span class="ag-btn shareScreenBtn" title="Share Screen">
<i class="ag-icon icon-screen-share"></i>
</span>
<span class="ag-btn displayModeBtn" title="Switch Display Mode">
<i class="ag-icon icon-switch-layout"></i>
</span>
The meeting functionality code is contained the src/pages/meeting/meeting.js
file.
- Define Global Variables
- Define the
optionsInit
Constant - Define the
uiInit
Constant - Define the
clientInit
Constant - Define the
streamInit
Constant - Define the
shareEnd
Constant - Define the
shareStart
Constant - Add Window Event Listeners and Callbacks
- Define the
removeStream
Constant - Define the
addStream
Constant - Define the
getStreamById
Constant - Define the
enableDualStream
Constant - Define the
setHighStream
Constant - Define the
subscribeStreamEvents
Constant - Define the
subscribeMouseEvents
Constant - Define the
infoDetectSchedule
Constant - Initialize the Page
Define the following local variables:
Variable | Value | Description |
---|---|---|
DUAL_STREAM_DEBUG |
false |
Enables/disables dual stream debugging |
options |
Empty object | Agora client settings |
client |
Empty object | Agora client |
localStream |
Empty object | Local stream |
streamList |
Empty array | List of streams |
shareClient |
null |
Agora client for sharing |
shareStream |
null |
Stream for sharing |
mainId |
N/A | ID for the main stream |
mainStream |
N/A | The main stream |
globalLog |
logger.init('global', 'blue') |
Global logging object |
shareLog |
logger.init('share', 'yellow') |
Share logging object |
localLog |
logger.init('local', 'green') |
Local logging object |
// If display a window to show video info
const DUAL_STREAM_DEBUG = false;
let options = {};
let client = {};
let localStream = {};
let streamList = [];
let shareClient = null;
let shareStream = null;
let mainId;
let mainStream;
const globalLog = logger.init('global', 'blue');
const shareLog = logger.init('share', 'yellow');
const localLog = logger.init('local', 'green');
The optionsInit
constant defines the settings for the Agora client and returns options
.
const optionsInit = () => {
...
return options;
};
Create an options
object with the following information.
Name | Value | Description |
---|---|---|
videoProfile |
`Cookies.get('videoProfile').split(',')[0] | |
videoProfileLow |
Cookies.get('videoProfileLow') |
Low stream video profile value from browser cookie data. |
cameraId |
Cookies.get('cameraId') |
Video device ID from browser cookie data. |
microphoneId |
Cookies.get('microphoneId') |
Audio device ID from browser cookie data. |
channel |
`Cookies.get('channel') | |
transcode |
`Cookies.get('transcode') | |
attendeeMode |
`Cookies.get('attendeeMode') | |
baseMode |
`Cookies.get('baseMode') | |
displayMode |
1 |
Display mode of the meeting. 0 to tile the video, 1 to display as PIP, 2 to screen share. |
uid |
undefined |
User ID set to undefined because it is dynamically generated. |
let options = {
videoProfile: Cookies.get('videoProfile').split(',')[0] || '480p_4',
videoProfileLow: Cookies.get('videoProfileLow'),
cameraId: Cookies.get('cameraId'),
microphoneId: Cookies.get('microphoneId'),
channel: Cookies.get('channel') || 'test',
transcode: Cookies.get('transcode') || 'interop',
attendeeMode: Cookies.get('attendeeMode') || 'video',
baseMode: Cookies.get('baseMode') || 'avc',
displayMode: 1, // 0 Tile, 1 PIP, 2 screen share
uid: undefined, // In default it is dynamically generated
resolution: undefined
};
Retrieve the temporary profile by using the videoProfile
browser cookie as the index for the RESOLUTION_ARR
array.
Calculate and set options.resolution
using tempProfile
.
If options.baseMode
is equal to avc
, set options.key
to the App ID.
let tempProfile = RESOLUTION_ARR[Cookies.get('videoProfile')];
options.resolution = tempProfile[0] / tempProfile[1] || 4 / 3;
if (options.baseMode === 'avc') {
options.key = APP_ID;
}
The uiInit
constant is used to initialize the meeting page of the sample application.
- Initialize the renderer with
ag-canvas
usingRenderer.init()
- If the browser is mobile sized, invoke
Renderer.enterFullScreen()
- If the browser is not Firefox or Chrome, disable the share screen button using
ButtonControl.disable()
- Set the room name in the
room-name
UI element with the valueoptions.channel
using$('#room-name').html()
- Hide the UI button controls depending on the value of
options.attendeeMode
usingButtonControl.hide()
attendeeMode Value |
Button classes to hide |
---|---|
audio-only |
videoControlBtn and shareScreenBtn |
audience |
videoControlBtn , audioControlBtn , and shareScreenBtn |
video (default) |
N/A |
const uiInit = options => {
Renderer.init('ag-canvas', 9 / 16, 8 / 5);
// Mobile page should remove title and footer
if (isMobileSize()) {
Renderer.enterFullScreen();
}
// Only firefox and chrome support screen sharing
if (!isFirefox() && !isChrome()) {
ButtonControl.disable('.shareScreenBtn');
}
$('#room-name').html(options.channel);
switch (options.attendeeMode) {
case 'audio-only':
ButtonControl.hide(['.videoControlBtn', '.shareScreenBtn']);
break;
case 'audience':
ButtonControl.hide(['.videoControlBtn', '.audioControlBtn', '.shareScreenBtn']);
break;
default:
case 'video':
break;
}
};
The clientInit()
constant is used to initialize the Agora client for the meeting page.
Initialize the Agora client using client.init()
and execute the following:
- Add a global log using
globalLog()
- Set a local variable
lowStreamParam
applyingoptions.videoProfileLow
as the index of theRESOLUTION_ARR
array - Invoke
client.join()
The remaining code in this section pertains to the client.join()
callback.
const clientInit = (client, options) => {
return new Promise((resolve, reject) => {
client.init(options.key, () => {
globalLog('AgoraRTC client initialized');
let lowStreamParam = RESOLUTION_ARR[options.videoProfileLow];
client.join(
...
);
});
});
};
The client.join()
method passes in the following parameters:
Parameter|Description
---|---|---
options.key
|Agora App ID
options.channel
|Channel name for the device tests
options.uid
|Unique channel name for the session, uses uid
as a default
uid
|User ID callback logs the uid
, sets the low stream parameter using client.setLowStreamParameter()
, and invokes resolve()
err
|Error callback invokes reject()
options.key,
options.channel,
options.uid,
uid => {
log(uid, 'brown', `User ${uid} join channel successfully`);
log(uid, 'brown', new Date().toLocaleTimeString());
client.setLowStreamParameter({
width: lowStreamParam[0],
height: lowStreamParam[1],
framerate: lowStreamParam[2],
bitrate: lowStreamParam[3]
});
// Create localstream
resolve(uid);
},
err => {
reject(err);
}
The streamInit()
constant initializes the stream.
/**
*
* @param {*} uid
* @param {*} options global option
* @param {*} config stream config
*/
const streamInit = (uid, options, config) => {
...
};
Create a defaultConfig
object with the following information:
Config Name | Value | Description |
---|---|---|
streamID |
uid |
Stream ID. Uses the User ID as default. |
audio |
true |
Enable audio indicator. |
video |
true |
Enable video indicator. |
screen |
false |
Share screen indicator. |
let defaultConfig = {
streamID: uid,
audio: true,
video: true,
screen: false
};
Set the video
and audio
properties for defaultConfig
to false
depending on the value of options.attendeeMode
attendeeMode |
Properties set to false |
---|---|
audio-only |
video |
audience |
video and audio |
video (default) |
N/A |
switch (options.attendeeMode) {
case 'audio-only':
defaultConfig.video = false;
break;
case 'audience':
defaultConfig.video = false;
defaultConfig.audio = false;
break;
default:
case 'video':
break;
}
Create the stream using AgoraRTC.createStream()
with the merged defaultConfig
and config
and return the resulting stream
.
// eslint-disable-next-line
let stream = AgoraRTC.createStream(merge(defaultConfig, config));
stream.setVideoProfile(options.videoProfile);
return stream;
The shareEnd()
constant ends sharing for the stream.
Execute the following before setting shareClient
and shareStream
to null
:
- If
shareClient
exists and unpublishing the shared stream usingclient.unpublish(shareStream)
is successful. - If
shareStream
exists and closing the stream usingshareStream.close()
is successful. - If
shareClient
exists and leaving the client usingshareClient.leave()
is successful.
const shareEnd = () => {
try {
shareClient && shareClient.unpublish(shareStream);
shareStream && shareStream.close();
shareClient &&
shareClient.leave(
() => {
shareLog('Share client succeed to leave.');
},
() => {
shareLog('Share client failed to leave.');
}
);
} finally {
shareClient = null;
shareStream = null;
}
};
The shareStart()
constant starts sharing for the stream.
- Disable the
shareScreenBtn
elements usingButtonControl.disable()
. - Create the share client using
AgoraRTC.createClient()
with theoptions.transcode
mode. - Create a local variable
shareOptions
with the merged value ofoptions
and{uid: SHARE_ID}
usingmerge()
.
Initialize the client using clientInit()
. The remaining code in this section are contained within the completion of clientInit()
using then()
.
const shareStart = () => {
ButtonControl.disable('.shareScreenBtn');
// eslint-disable-next-line
shareClient = AgoraRTC.createClient({
mode: options.transcode
});
let shareOptions = merge(options, {
uid: SHARE_ID
});
clientInit(shareClient, shareOptions).then(uid => {
...
});
};
Create a config
object and apply it to the stream creation using streamInit()
. The config
properties contain the following information:
Config Name | Value | Description |
---|---|---|
screen |
true |
Enable share screen indicator. |
audio |
true |
Enable audio indicator. |
video |
true |
Enable video indicator. |
extensionId |
minllpmhdgpndnkomcoccfekfegnlikg |
The extension ID for the sample application. |
mediaSource |
application |
The media source for the sample application. |
let config = {
screen: true,
video: false,
audio: false,
extensionId: 'minllpmhdgpndnkomcoccfekfegnlikg',
mediaSource: 'application'
};
shareStream = streamInit(uid, shareOptions, config);
Initialize the stream creation using shareStream.init()
.
If successful:
- Enable the
shareScreenBtn
UI element usingButtonControl.enable()
. - Add a
stopScreenSharing
event listener toshareStream
. The listener invokesshareEnd()
and sets a share log usingshareLog()
. - Publish the stream using
shareClient.publish()
and add any errors to the share log usingshareLog()
.
If unsuccessful:
- Enable the
shareScreenBtn
UI element usingButtonControl.enable()
. - Add a share log for the error using
shareLog()
. - Invoke
shareEnd()
. - If the browser is a Chrome browser, notify the user to install the required Chrome extension.
shareStream.init(
() => {
ButtonControl.enable('.shareScreenBtn');
shareStream.on('stopScreenSharing', () => {
shareEnd();
shareLog('Stop Screen Sharing at' + new Date());
});
shareClient.publish(shareStream, err => {
shareLog('Publish share stream error: ' + err);
shareLog('getUserMedia failed', err);
});
},
err => {
ButtonControl.enable('.shareScreenBtn');
shareLog('getUserMedia failed', err);
shareEnd();
if (isChrome()) {
let msg = `Please install chrome extension before using sharing screen.
<hr />
<a id="addExtensionBtn" class="button is-link" onclick="chrome.webstore.install('https://chrome.google.com/webstore/detail/minllpmhdgpndnkomcoccfekfegnlikg', installSuccess, installError)">Add chrome extension</a>
`;
Notify.danger(msg, 5000);
}
}
);
Add event listeners to the browser window.
If the extension install is successful, set a global log using globalLog()
.
window.installSuccess = (...args) => {
globalLog(...args);
};
If the extension install is not successful, set a global log using globalLog()
and notify the user of the failed installation using Notify.danger()
.
window.installError = (...args) => {
globalLog(...args);
Notify.danger(
'Failed to install the extension, please check the network and console.',
3000
);
};
The removeStream()
constant removes the stream.
Iterate through the streamList
array and update the UI. If the stream is the current stream:
- Close the stream using
streamList[index].close()
. - Remove the video item using
$('#video-item-' + id).remove()
. - Remove the stream from the
streamList
array usingstreamList.splice()
. - Return
1
.
If the length of streamList
is less than or equal to 4
and options.displayMode
is not equal to 2
, enable the displayModeBtn
UI elements.
Render the streamList
using Renderer.customRender()
.
const removeStream = id => {
streamList.map((item, index) => {
if (item.getId() === id) {
streamList[index].close();
$('#video-item-' + id).remove();
streamList.splice(index, 1);
return 1;
}
return 0;
});
if (streamList.length <= 4 && options.displayMode !== 2) {
ButtonControl.enable('.displayModeBtn');
}
Renderer.customRender(streamList, options.displayMode, mainId);
};
The addStream()
constant adds the stream.
- Set the stream
id
using the valuestream.getId()
. - Check for
redundant
streams usingstreamList.some()
. - If a
redundant
stream is found invokereturn
. - Push the
stream
tostreamList
usingstreamList.push()
and resort the list usingstreamList.unshift()
. - If the length of
streamList
is greater than or equal to4
andoptions.displayMode
is equal to1
, setoptions.displayMode
to0
. - Render the
streamList
usingRenderer.customRender()
.
const addStream = (stream, push = false) => {
let id = stream.getId();
// Check for redundant
let redundant = streamList.some(item => {
return item.getId() === id;
});
if (redundant) {
return;
}
// Do push for localStream and unshift for other streams
push ? streamList.push(stream) : streamList.unshift(stream);
if (streamList.length > 4) {
options.displayMode = options.displayMode === 1 ? 0 : options.displayMode;
ButtonControl.disable(['.displayModeBtn', '.disableRemoteBtn']);
}
Renderer.customRender(streamList, options.displayMode, mainId);
};
The getStreamById()
constant retrieves the stream based on its stream id
by returning the result of streamList.filter()
.
const getStreamById = id => {
return streamList.filter(item => {
return item.getId() === id;
})[0];
};
The enableDualStream()
constant enables dual stream mode using client.enableDualStream()
and sets a local log using localLog()
upon success or failure.
const enableDualStream = () => {
client.enableDualStream(
function() {
localLog('Enable dual stream success!');
},
function(e) {
localLog(e);
}
);
};
The setHighStream()
constant sets updates the previous and next stream resolution settings.
If the previous stream is the same as the next
stream, invoke return
.
Iterate through streamList
:
- If the current stream's
id
is equal toprev
, setprevStream
tostream
- If the current stream's
id
is equal tonext
, setnextStream
tostream
If prevStream
is valid, set the remote video stream type to 1
using client.setRemoteVideoStreamType()
.
If nextStream
is valid, set the remote video stream type to 0
using client.setRemoteVideoStreamType()
.
const setHighStream = (prev, next) => {
if (prev === next) {
return;
}
let prevStream;
let nextStream;
// Get stream by id
for (let stream of streamList) {
let id = stream.getId();
if (id === prev) {
prevStream = stream;
} else if (id === next) {
nextStream = stream;
} else {
// Do nothing
}
}
// Set prev stream to low
prevStream && client.setRemoteVideoStreamType(prevStream, 1);
// Set next stream to high
nextStream && client.setRemoteVideoStreamType(nextStream, 0);
};
The subscribeStreamEvents
constant adds event listeners to the stream.
/**
* Add callback for client event to control streams
* @param {*} client
* @param {*} streamList
*/
const subscribeStreamEvents = () => {
...
};
- Add a Stream Add Event Listener and Callback
- Add a Peer Leave Event Listener and Callback
- Add a Stream Subscribed Event Listener and Callback
- Add a Stream Removed Event Listener and Callback
Add a stream-added
event listener to the client
.
- Set a local
stream
variable with the valueevt.stream
- Set a local
id
variable with the valuestream.getId()
- Add a local log for the
id
usinglocalLog()
- Add a local log for the date/time using
localLog()
- Add a local log for the stream using
localLog()
- If
id
is equal toSHARE_ID
- Set
options.displayMode
to2
- Set
mainId
toid
- Set
mainStream
tostream
- Disable the
shareScreenBtn
UI elements ifshareClient
is invalid usingButtonControl.disable()
- Disable the UI elements whose classes are
displayModeBtn
anddisableRemoteBtn
usingButtonControl.disable()
- Set
- If
id
is equal tomainId
- If
options.displayMode
is equal to2
, set the remote video stream type forstream
usingclient.setRemoteVideoStreamType()
- Otherwise, ensure
mainStream
is valid and set the remote video stream type formainStream
usingclient.setRemoteVideoStreamType()
and updatemainStream
tostream
andmainId
toid
- If
- Subscribe the stream to the
client
usingclient.subscribe()
client.on('stream-added', function(evt) {
let stream = evt.stream;
let id = stream.getId();
localLog('New stream added: ' + id);
localLog(new Date().toLocaleTimeString());
localLog('Subscribe ', stream);
if (id === SHARE_ID) {
options.displayMode = 2;
mainId = id;
mainStream = stream;
if (!shareClient) {
ButtonControl.disable('.shareScreenBtn');
}
ButtonControl.disable(['.displayModeBtn', '.disableRemoteBtn']);
}
if (id !== mainId) {
if (options.displayMode === 2) {
client.setRemoteVideoStreamType(stream, 1);
} else {
mainStream && client.setRemoteVideoStreamType(mainStream, 1);
mainStream = stream;
mainId = id;
}
}
client.subscribe(stream, function(err) {
localLog('Subscribe stream failed', err);
});
});
Add a peer-leave
event listener to the client
.
- Set a local
id
variable with the valueevt.uid
- Set a local log for the
id
usinglocalLog()
- Set a local log for the date/time using
localLog()
- If
id
is equal toSHARE_ID
- Set
options.displayMode
to0
- Set
mainId
toid
- Enable the
shareScreenBtn
UI elements ifoptions.attendeeMode
is equal tovideo
usingButtonControl.enable()
- Enable the UI elements whose classes are
displayModeBtn
anddisableRemoteBtn
usingButtonControl.enable()
- Set
- If
id
is equal tomainId
- If
options.displayMode
is equal to2
, setnext
toSHARE_ID
- Otherwise, set the value to
localStream.getId()
- Set
mainId
tonext
- Set
mainStream
the with value ofgetStreamById()
- If
- Remove the stream using
removeStream()
client.on('peer-leave', function(evt) {
let id = evt.uid;
localLog('Peer has left: ' + id);
localLog(new Date().toLocaleTimeString());
if (id === SHARE_ID) {
options.displayMode = 0;
if (options.attendeeMode === 'video') {
ButtonControl.enable('.shareScreenBtn');
}
ButtonControl.enable(['.displayModeBtn', '.disableRemoteBtn']);
shareEnd();
}
if (id === mainId) {
let next = options.displayMode === 2 ? SHARE_ID : localStream.getId();
setHighStream(mainId, next);
mainId = next;
mainStream = getStreamById(mainId);
}
removeStream(evt.uid);
});
Add a stream-subscribed
event listener to the client
.
- Set the a local variable
stream
toevt.stream
- Set local logs for the event listener and date/time using
localLog()
- Add the
stream
usingaddStream()
client.on('stream-subscribed', function(evt) {
let stream = evt.stream;
localLog('Got stream-subscribed event');
localLog(new Date().toLocaleTimeString());
localLog('Subscribe remote stream successfully: ' + stream.getId());
addStream(stream);
});
Add a stream-removed
event listener to the client
.
- Set the a local variable
stream
toevt.stream
. - Set the a local variable
id
tostream.getId()
. - Set local logs for the
id
and date/time usinglocalLog()
. - If
id
is equal toSHARE_ID
, do the following:- Set
options.displayMode
to0
. - If
options.attendeeMode
is equal tovideo
, enable theshareScreenBtn
UI elements usingButtonControl.enable()
. - Use
ButtonControl.enable()
to enable UI elements whose classes aredisplayModeBtn
anddisableRemoteBtn
. - Invoke
shareEnd()
.
- Set
- If
id
is equal tomainId
, do the following:- If
options.displayMode
is equal to2
, setnext
toSHARE_ID
. Otherwise, set the value tolocalStream.getId()
. - Invoke
setHighStream()
withmainId
andnext
. - Set
mainId
tonext
. - Set
mainStream
the with value ofgetStreamById()
.
- If
- Remove the stream using
removeStream()
.
client.on('stream-removed', function(evt) {
let stream = evt.stream;
let id = stream.getId();
localLog('Stream removed: ' + id);
localLog(new Date().toLocaleTimeString());
if (id === SHARE_ID) {
options.displayMode = 0;
if (options.attendeeMode === 'video') {
ButtonControl.enable('.shareScreenBtn');
}
ButtonControl.enable(['.displayModeBtn', '.disableRemoteBtn']);
shareEnd();
}
if (id === mainId) {
let next = options.displayMode === 2 ? SHARE_ID : localStream.getId();
setHighStream(mainId, next);
mainId = next;
mainStream = getStreamById(mainId);
}
removeStream(stream.getId());
});
The subscribeMouseEvents
constant adds event listeners to the UI elements.
const subscribeMouseEvents = () => {
...
};
- Add a
displayModeBtn
Click Event Listener and Callback - Add an
exitBtn
Click Event Listener and Callback - Add an
ag-videoControlBtn
Click Event Listener and Callback - Add an
ag-shareScreenBtn
Click Event Listener and Callback - Add an
ag-disableRemoteBtn
Click Event Listener and Callback - Add a Resize Window Event Listener and Callback
- Add an
ag-container
Double Click Event Listener and Callback - Add a Document Mouse Move Event Listener and Callback
Add a click
event listener to the displayModeBtn
element.
- If the
currentTarget
contains thedisabled
class or the length ofstreamList
is1
, invokereturn
. - If
options.displayMode
is equal to1
, setoptions.displayMode
to0
and disable thedisableRemoteBtn
elements usingButtonControl.disable()
. - If
options.displayMode
is equal to0
, setoptions.displayMode
to1
and enable thedisableRemoteBtn
elements usingButtonControl.enable()
. - Invoke
Renderer.customRender()
withstreamList
,options.displayMode
, andmainId
.
$('.displayModeBtn').on('click', function(e) {
if (e.currentTarget.classList.contains('disabled') || streamList.length <= 1) {
return;
}
// 1 refer to pip mode
if (options.displayMode === 1) {
options.displayMode = 0;
ButtonControl.disable('.disableRemoteBtn');
} else if (options.displayMode === 0) {
options.displayMode = 1;
ButtonControl.enable('.disableRemoteBtn');
} else {
// Do nothing when in screen share mode
}
Renderer.customRender(streamList, options.displayMode, mainId);
});
Add a click
event listener to the exitBtn
element. Execute the following before redirecting to the index.html page.
- If the
shareClient
is valid invokeshareEnd()
. - If
client
is valid, unpublishlocalStream
client.unpublish()
. - If
client
is valid, leave the channel usingclient.leave()
.
$('.exitBtn').on('click', function() {
try {
shareClient && shareEnd();
client && client.unpublish(localStream);
localStream && localStream.close();
client &&
client.leave(
() => {
localLog('Client succeed to leave.');
},
() => {
localLog('Client failed to leave.');
}
);
} finally {
// Redirect to index
window.location.href = 'index.html';
}
});
Add a click
event listener to the videoControlBtn
and audioControlBtn
elements.
- Toggle the button class to
off
usingtoggleClass()
. - If video or audio is on, disable/enable video or audio using
localStream.disableVideo()
andlocalStream.enableVideo()
orlocalStream.disableAudio()
andlocalStream.enableAudio()
.
$('.videoControlBtn').on('click', function() {
$('.videoControlBtn').toggleClass('off');
localStream.isVideoOn() ? localStream.disableVideo() : localStream.enableVideo();
});
$('.audioControlBtn').on('click', function() {
$('.audioControlBtn').toggleClass('off');
localStream.isAudioOn() ? localStream.disableAudio() : localStream.enableAudio();
});
Add a click
event listener to the shareScreenBtn
element.
- If the
currentTarget
contains thedisabled
class, invokereturn
. - If
shareClient
is valid, invokeshareEnd()
. Otherwise, invokeshareStart()
.
$('.shareScreenBtn').on('click', function(e) {
if (e.currentTarget.classList.contains('disabled')) {
return;
}
if (shareClient) {
shareEnd();
} else {
shareStart();
}
});
Add a click
event listener to the disableRemoteBtn
element.
- If the
currentTarget
contains thedisabled
class or the length ofstreamList
is1
, invokereturn
. - Toggle the
disableRemoteBtn
UI element class tooff
using$('.disableRemoteBtn').toggleClass()
. - Set a local
id
variable tolocalStream.getId()
. - Retrieve the
list
ofvideo-item
elements. - Iterate through the
list
.- If
item.style.display
is equal tonone
, set the display style toblock
and return1
. - Set
item.style.display
tonone
. - Return
0
.
- If
$('.disableRemoteBtn').on('click', function(e) {
if (e.currentTarget.classList.contains('disabled') || streamList.length <= 1) {
return;
}
$('.disableRemoteBtn').toggleClass('off');
let list;
let id = localStream.getId();
list = Array.from(document.querySelectorAll(`.video-item:not(#video-item-${id})`));
list.map(item => {
if (item.style.display === 'none') {
item.style.display = 'block';
return 1;
}
item.style.display = 'none';
return 0;
});
});
Add a resize
event listener to the window
.
If the browser is mobile sized, invoke Renderer.enterFullScreen()
. Otherwise, invoke Renderer.exitFullScreen()
.
Render the streamList
with the options.displayMode
configuration for mainId
using Renderer.customRender()
.
$(window).resize(function(_) {
if (isMobileSize()) {
Renderer.enterFullScreen();
} else {
Renderer.exitFullScreen();
}
Renderer.customRender(streamList, options.displayMode, mainId);
});
Add a dblclick
event listener to the container
element.
- Set a
dom
local variable toe.target
. - Iterate through the
dom
class list and search for the classvideo-item
. If found, setdom
todom.parentNode
. If thedom
class list has the classag-main
, invokereturn
. - Calculate and set
id
usingdom
. - If
id
is equal tomainId
, do the following:- If
options.displayMode
is equal to2
, setnext
toSHARE_ID
. Otherwise, set the value toid
. - Invoke
setHighStream()
formainId
. - Set
mainId
tonext
. - Set
mainStream
to the result ofgetStreamById()
.
- If
- Invoke
Renderer.customRender()
forstreamList
with the configurationoptions.displayMode
formainId
.
// Dbl click to switch high/low stream
$('.ag-container').dblclick(function(e) {
let dom = e.target;
while (!dom.classList.contains('video-item')) {
dom = dom.parentNode;
if (dom.classList.contains('ag-main')) {
return;
}
}
let id = parseInt(dom.id.split('-')[2], 10);
if (id !== mainId) {
let next = options.displayMode === 2 ? SHARE_ID : id;
// Force to swtich
setHighStream(mainId, next);
mainId = next;
mainStream = getStreamById(mainId);
}
Renderer.customRender(streamList, options.displayMode, mainId);
});
Add a mousemove
event listener to the document
.
- If
global._toolbarToggle
istrue
, clear theglobal._toolbarToggle
usingclearTimeout()
. - Add the
active
class to theag-btn-group
UI element. - Set
global._toolbarToggle
withsetTimeout()
applying theactive
class to theag-btn-group
UI element and a timeout of2500
milliseconds.
$(document).mousemove(function(_) {
if (global._toolbarToggle) {
clearTimeout(global._toolbarToggle);
}
$('.ag-btn-group').addClass('active');
global._toolbarToggle = setTimeout(function() {
$('.ag-btn-group').removeClass('active');
}, 2500);
});
The infoDetectSchedule
updates the statistics for the sample application.
Set a local no
variable to streamList.length
.
Iterate through the stream list. The remaining code in this section is contained within the for
loop.
const infoDetectSchedule = () => {
let no = streamList.length;
for (let i = 0; i < no; i++) {
...
}
};
Set the following local variables:
Variable | Value | Description |
---|---|---|
item |
streamList[i] |
Stream list item |
id |
item.getId() |
Item ID |
box |
$('#video-item-${id} .video-item-box') |
Video item UI element |
width |
N/A | Width of the video resolution |
height |
N/A | Height of the video resolution |
frameRate |
N/A | Frame rate of the video |
HighOrLow |
N/A | Indicates high or low stream |
let item = streamList[i];
let id = item.getId();
let box = $('#video-item-${id} .video-item-box');
let width;
let height;
let frameRate;
let HighOrLow;
- If the
id
is equal tomainId
, setHighOrLow
toHigh
. Otherwise, setHighOrLow
toLow
. - If
i
is equal tono - 1
, setHighOrLow
tolocal
.
// Whether high or low stream
if (id === mainId) {
HighOrLow = 'High';
} else {
HighOrLow = 'Low';
}
if (i === no - 1) {
HighOrLow = 'local';
}
Invoke item.getStats()
. After the callback, execute the following:
- If
i
is equal tono-1
, use the sent video resolution:- Set
width
toe.videoSendResolutionWidth
. - Set
height
toe.videoSendResolutionHeight
. - Set
frameRate
toe.videoSendFrameRate
.
- Set
- Otherwise, use the received video resolution:
- Set
width
toe.videoReceivedResolutionWidth
. - Set
height
toe.videoReceivedResolutionHeight
. - Set
frameRate
toe.videoReceiveFrameRate
.
- Set
- Set a local
str
variable with theid
,width
,height
,frameRate
, andHighOrLow
information using<p>
UI elements. - Set the inner HTML of
box
tostr
.
item.getStats(function(e) {
if (i === no - 1) {
width = e.videoSendResolutionWidth;
height = e.videoSendResolutionHeight;
frameRate = e.videoSendFrameRate;
} else {
width = e.videoReceivedResolutionWidth;
height = e.videoReceivedResolutionHeight;
frameRate = e.videoReceiveFrameRate;
}
let str = `
<p>uid: ${id}</p>
<p>${width}*${height} ${frameRate}fps</p>
<p>${HighOrLow}</p>
`;
box.html(str);
});
Initialize the meeting page.
- Set
options
to the result ofoptionsInit()
. - Invoke
uiInit()
to apply theoptions
to the UI. - Create the Agora client using
AgoraRTC.createClient()
with theoptions.transcode
mode. - Invoke
subscribeMouseEvents()
to register mouse event listeners. - Invoke
subscribeStreamEvents()
to register stream event listeners. - Invoke
clientInit()
withclient
andoptions
. - If
DUAL_STREAM_DEBUG
istrue
, set an interval schedule forinfoDetectSchedule
usingsetInterval
every1000
milliseconds.
The remaining code in this section is contained within the clientInit()
callback method then()
.
// ------------- start --------------
// ----------------------------------
options = optionsInit();
uiInit(options);
// eslint-disable-next-line
client = AgoraRTC.createClient({
mode: options.transcode
});
subscribeMouseEvents();
subscribeStreamEvents();
clientInit(client, options).then(uid => {
...
});
if (DUAL_STREAM_DEBUG) {
setInterval(infoDetectSchedule, 1000);
}
Set a local config
variable.
- If the browser is Safari, use an empty object.
- If the browser is not Safari, set
config
to an object withcameraId
of valueoptions.cameraId
andmicrophoneId
of valueoptions.microphoneId
.
Set the localStream
using streamInit()
with uid
, options
, and config
.
// Use selected device
let config = isSafari()
? {}
: {
cameraId: options.cameraId,
microphoneId: options.microphoneId
};
localStream = streamInit(uid, options, config);
If options.attendeeMode
is not equal to audience
, set mainId
to uid
and mainStream
to localStream
.
Invoke enableDualStream()
to enable dual stream mode.
// Enable dual stream
if (options.attendeeMode !== 'audience') {
// MainId default to be localStream's ID
mainId = uid;
mainStream = localStream;
}
enableDualStream();
Initialize the local stream using localStream.init()
.
If successful, check that options.attendeeMode
does not equal audience
.
- Add the stream using
addStream()
. - Publish it to the client using
client.publish()
.
localStream.init(
() => {
if (options.attendeeMode !== 'audience') {
addStream(localStream, true);
client.publish(localStream, err => {
localLog('Publish local stream error: ' + err);
});
}
},
err => {
localLog('getUserMedia failed', err);
}
);
- Complete API documentation is available at the Document Center.
- You can file bugs about this sample here.
This software is under the MIT License (MIT). View the license.