The objective of this tutorial is to achieve a call with several partners and get used to basic events of the SippoSDK session.
We are going to use the previously created app and upgrade it to handle calls.
To perform this tutorial we will take as an starting point the result of the tutorial 2 - Register
.
So on the following link you will find the following structure:
project/
├──js/
| ├── libs/
| │ └── sippojs-janus-24.2.0.js
| ├── services/
| │ └── auth.js
| ├── utils/
| │ └── logger.js
| └── app.js
├── index.html
└── style.css
We are going to create a new service called CallService
for this tutorial.
We will work with the following files:
index.html
: Includes a new panel to work on this tutorial. We will go for it on next steps.app.js
: Initialize the services and expose the public methods to the interface.call.js
: Service that allow us to use 1-to-1 calls.The first step is to analyze all the Sippo classes, methods and events that we will use for creating our application. This way we will understand better how Sippo works and we will discover new features that can be implemented.
Take your time to check all of them and discover all their capabilities.
The main classes that we will use for implementing our new features are the following:
ConferenceManager.createConference()
method or handling the onIncoming event of a connected ConferenceManager instance.Session.getConferenceManager()
.These are the main methods that we will use:
And finally we will listen the following events from the ConferenceManager
and Conference
classes:
conferenceInvitation
: Emitted every time an invitation to a conference room is received.localStreamAdded
: Emitted on a new local stream.remoteStreams
: Emitted every time remoteStreams changes.The first step is to add a new panel to the interface:
<div class="panel">
<h3>Call panel</h3>
<button onclick="createAndJoin()">Create & join room</button>
<button onclick="leave()">Leave room</button>
<button onclick="answer()">Answer</button> <br>
<label>Callee:</label>
<input id="call-callee" type="text">
<button onclick="invite()">Invite</button> <br>
<div class="call-video-panel">
<h4>Local video</h4>
<video id="call-local-video" autoplay playsinline></video>
</div>
<div class="call-video-panel">
<h4>Remote videos</h4>
<div id="call-remote-videos"></div>
</div>
<audio id="call-audio" autoplay></audio>
</div>
You will notice that the video element has two attributes: autoplay
and playsinline
. Both attributes are necessary to play the video automatically. If we don't include them, we will see the video frozen. In special, the second attribute is to be able to autoplay the videos in Safari.
Finally, do not forget to add the service that we will create to our index.html
:
<!-- App services -->
<script src="js/services/auth.js"></script>
<script src="js/services/call.js"></script>
Now is the turn of defining the public functions. The goal of these functions is to be used as an interconnection layer between the user interface and the CallService
. These function will be called when the user press a button.
You will see that we use some methods from the CallService
. Don't worry, because we will see their implementation later.
The first step is to create an instance of the CallService
at the beginning of the file:
// Define the services
let logger = new LoggerService();
let authSvc = new AuthService();
let callSvc = new CallService();
In the next subsections we will define all the new functions that are defined in this file.
The first step is to pass to the CallService
all the HTML elements that will be used to attach the media streams.
Then we call the createAndJoin()
method that is defined inside the CallService
.
function createAndJoin() {
logger.log('Trying to create and login into conference');
let localVideoEl = document.getElementById('call-local-video');
let remoteVideosEl = document.getElementById('call-remote-videos');
let audioEl = document.getElementById('call-audio');
callSvc.setLocalVideoEl(localVideoEl);
callSvc.setRemoteVideosEl(remoteVideosEl);
callSvc.setAudioEl(audioEl);
callSvc.createAndJoin().then( () => {
logger.succ('Conference created and join to it.');
}).catch( () => {
logger.error('Cannot create and join the conference. Are you registered?');
});
}
Take into account that after running this method, we will enter in an empty room. We will see how to invite a user in the next steps.
We only call the method with the same name from the CallService
:
function leave() {
callSvc.leave().then( () => {
logger.succ('Conference leaving successful.');
}).catch( () => {
logger.error('Cannot leave. Are you in a conference?');
});
}
It works in similar way that the createAndJoin()
function. We pass all the HTML elements to the CallService
and launch the answer()
method:
function answer() {
let localVideoEl = document.getElementById('call-local-video');
let remoteVideosEl = document.getElementById('call-remote-videos');
let audioEl = document.getElementById('call-audio');
callSvc.setLocalVideoEl(localVideoEl);
callSvc.setRemoteVideosEl(remoteVideosEl);
callSvc.setAudioEl(audioEl);
logger.log('Trying to answer.');
callSvc.answer().then( () => {
logger.succ('Answer successful.')
}).catch( () => {
logger.error('Cannot answer. Are you receiving a call?');
});
}
Now is the turn of adding a new participant to an ongoing call. We get the user from the input
HTML element and we pass its value to the invite(callee: string)
method that is defined in the CallService
:
function invite() {
let callee = document.getElementById('call-callee').value;
logger.log('Trying to invite to ' + callee + '.');
callSvc.invite(callee).then( () => {
logger.succ('Invite sent to ' + callee);
}).catch( () => {
logger.error('Cannot send invite. Are you in a conference?');
});
}
Finally, we modify the _refreshSession()
method and add the setter for the session inside the CallService
:
function _refreshSession() {
// Save the session in the other services
let session = authSvc.getSession();
callSvc.setSession(session);
}
Now is the turn of creating the skeleton for our brand new service. Notice that in the constructor we define all the variables that need to be shared between methods.
var CallService = (function() {
class CallService {
constructor() {
this.logger = new LoggerService();
this.session = null;
this.conferenceManager = null;
this.currentConference = null;
this.localVideoEl = null;
this.remoteVideosEl = null;
this.audioEl = null;
}
}
return CallService;
}());
In the following subsections we will define all the methods that are included in the CallService
.
We need to include some methods to modify the current values of the variables inside our CallService
instance.
When we change the session, we also bind an event to the ConferenceManager
. This event is called conferenceInvitation
and it will be triggered every time we receive a conference invitation from other user:
setSession(session) {
this.session = session;
if (session) {
this.conferenceManager = this.session.getConferenceManager();
this.conferenceManager.emitter.on('conferenceInvitation',
this._onConferenceInvitation.bind(this));
} else {
this.conferenceManager.emitter.off('conferenceInvitation',
this._onConferenceInvitation.bind(this));
this.conferenceManager = null;
}
}
We also need to initialize the HTML element in which we will attach the media streams:
setLocalVideoEl(element) {
this.localVideoEl = element;
}
setRemoteVideosEl(element) {
this.remoteVideosEl = element;
}
setAudioEl(element) {
this.audioEl = element;
}
We create a new conference and bind some events to this conference. This way, if we detect any change in the media streams, we can attach these new streams to the interface.
Finally, we join to the conference:
async createAndJoin() {
if (!this.session) {
return Promise.reject();
}
this.currentConference = await this.conferenceManager.createConference();
this.currentConference.emitter.on('localStreamAdded',
this._onLocalStreamAdded.bind(this));
this.currentConference.emitter.on('remoteStreams',
this._onRemoteStreams.bind(this));
return await this.currentConference.join();
}
In case we want to get out of a conference, we can use the leave(codeError: number)
method. The value of the codeError is the SIP error code indicating the cause of leaving. Normally, it will be 200.
Although we leave the conference, the rest of the participants can continue with it:
leave() {
if (!this.session || !this.currentConference) {
return Promise.reject();
}
return this.currentConference.leave(200).then( () => {
this.currentConference = null;
});
}
This method can be launched by the user when a conferenceInvitation
was previously received. In this case we bind the events and launch the join()
method from Conference
. Just the same way that we have done for createAndJoin()
:
answer() {
if (!this.currentConference) {
return Promise.reject();
}
this.currentConference.emitter.on('localStreamAdded',
this._onLocalStreamAdded.bind(this));
this.currentConference.emitter.on('remoteStreams',
this._onRemoteStreams.bind(this));
return this.currentConference.join();
}
We check that a Session
and a Conference
is defined and, if it is the case, we call the inviteParticipants()
method from the Conference
instance:
invite(callee) {
if (!this.session || !this.currentConference) {
return Promise.reject();
}
return this.currentConference.inviteParticipants([callee]);
}
This method will be launched every time we receive the event localStreams
. In this case we obtain the video stream from the LocalMediaHandler
and attach it to the <video>
tag from our HTML template:
_onLocalStreamAdded(newStream) {
this.logger.log('Local stream added received.');
this.logger.log('Attaching local video stream');
this.localVideoEl.srcObject = newStream.stream.mediaStream;
}
This method is launched every time a remoteStreams
event is received. The remote streams can be an audio or video stream. We will have a single audio stream with the audio of all the participants mixed and one video stream per remote participant.
The different between a type and the other is that the audio stream doesn't have a value for the gatewayUsername
. This means that its value will be null
.
We attach the audio stream directly to the <audio>
HTML tag. For the videos we create a video
element per participant and we attach it to a div
element:
_onRemoteStreams(streams) {
this.logger.log('Remote stream change received.');
this.remoteVideosEl.innerHTML = '';
streams.forEach( (stream, gatewayUsername) => {
if (gatewayUsername) {
this.logger.log('Attaching remote video stream.');
let video = document.createElement('video');
video.autoplay = true;
video.setAttribute('playsinline', '');
video.srcObject = stream[0].mediaStream;
this.remoteVideosEl.appendChild(video);
} else {
this.logger.log('Attaching audio stream.');
this.audioEl.srcObject = stream[0].mediaStream;
}
});
}
Finally, we define the method that will be used each time we detect an conference invitation. This method will receive a Conference
instance and we will save it in a class variable.
This is the conference that will be used when the user push the answer button:
_onConferenceInvitation(conference) {
this.logger.warn('Detected an incoming conference invitation. ' +
'Press the Answer button to accept it.');
this.currentConference = conference;
}
Now we have a developed a complete WebRTC application with audio and video capabilities, however we can improve even this with new features.
When executing, the result should be similar to the following one:
On the next tutorial we will modify the CallService
and add some interesting features such as mute audio, video, change the camera or share our screen.
Do not hesitate to contact Quobis if you have any question or to provide feedback info@quobis.com
#alwaysImproving