Recently, a function is to make a page call, using WebRTC technology, in fact, after using JsSIP, the difficulty is straight down to the use of library, the use of light learning library is meaningless, so we have to first understand the principle of WebRTC

The principle is briefly

WebRTC is point-to-point, the data channel is P2P, but still needs the support of the server, the role of the server is basically the transmission of WebRTC required signaling information, tell the browser side how to connect

In the connection process, Network Address Translation (NAT) penetration is an important point, which is also called hole drilling. To solve the ipv4 Address shortage, most clients reside in the router’s internal Network environment, for example, the internal Network Address is 192.168.1.3. In this case, you need to bypass the firewall and create a unique IP address visible on the public network. After NAT is implemented, this IP address is mapped to a public IP address + a unique port, for example, 182.150.184.98:52054

This penetration process typically uses Interactive Connectivity Establishment (ICE), the ICE protocol framework, where there are two types of servers, Session Traversal Utilities for NAT(STUN) server and Traversal Using Relays around NAT(TURN) server. Generally, only STUN server will be used. After obtaining the unique visible address of the public network, the two ends can establish a connection. However, if the router does not allow the host to connect directly, the TURN server is required to forward data

Illustration of STUN connection process

TURN connection process diagram

JsSIP stomp pit notes

The SIP protocol has little to do with WebRTC. WebRTC does not use SIP. SIP is used to initiate, maintain, and terminate real-time sessions, including voice, video, and message applications

JsSIP is mainly to parse SIP signaling, let us and the server know whether to call or answer the call now, incidentally encapsulates WebRTC things, no need to manually establish RTCPeerConnection

Check out the example on its website

var socket = new JsSIP.WebSocketInterface('wss://sip.myhost.com');

var configuration = {
  sockets  : [ socket ],
  uri      : 'sip:[email protected]'.password : 'superpassword'
};

var ua = new JsSIP.UA(configuration);

ua.start();

// Register callback events for calls
var eventHandlers = {
  'progress': function(e) {
    console.log('call is in progress');
  },
  'failed': function(e) {
    console.log('call failed with cause: '+ e.data.cause);
  },
  'ended': function(e) {
    console.log('call ended with cause: '+ e.data.cause);
  },
  'confirmed': function(e) {
    console.log('call confirmed'); }};var options = {
  'eventHandlers'    : eventHandlers,
  'mediaConstraints' : { 'audio': true.'video': true}};var session = ua.call('sip:[email protected]', options);
Copy the code

Let’s start with the basic process

  • Set up with the serversocketConnection –new JsSIP.WebSocketInterface
  • Create a UA object —new JsSIP.UA
  • Make a phone call —ua.call

The problem with it is that the event binding is a little confusing, and I don’t really understand it right now, but it has two different ways of event binding

  • uaBind the callback event
  • callFunction binding callback events

Ua Binding event

ua.on('connected', () = >console.log('[SIP Phone] : Connected (On Call)'));
ua.on('registered', () = >console.log('[SIP Phone] : Registered (On Call)'));
ua.on('registrationFailed', () = >console.log('[SIP Phone] : Registration Failed (On Call)'));
Copy the code

This is the event of connection, registration, registration failure, and the most important event is newRTCSession, which represents a new call

ua.on('newRTCSession', (e) => {
  // Get the session object and originator object
  // Originator indicates whether an originator is a local call or a remote call
  const { session, originator } = e;
  
  // The event bound to the session is the most important
  
  / / connection
  session.on('connecting', () = > {});// Connection accepted
  session.on('accepted', () = > {});// Switch on, in this step can handle audio playback
  // Put on does not mean the other party has accepted, put on means drip drip drip
  session.on('confirmed', () = > {});/ / end
  session.on('ended', () = > {});/ / fail
  session.on('failed', () = > {});// Manually let the drilling end, up to 4 times, sometimes the wait time is very long
  let iceCandidateCount = 0;
  session.on('icecandidate', (data) => {
    if (iceCandidateCount++ > 4) data.ready();
  });
}
Copy the code

The event is registered, so how do I play the audio?

Through consulting a lot of data, I found at least three ways

const audioElement; // Get the page audio element
ua.on('newRTCSession', (e) => {
  const { session } = e;
  // session.connection represents the RTCPeerConnection instance object
  session.on('confirmed', (event) => {
    // The first method is deprecated, but Chrome 80 is available. This API cannot be found on MDN
    audioElement.srcObject = session.connection.getRemoteStreams()[0];
    // The second method is deprecated, but still usable
    session.connection.onaddstream = (e) = > {
      audioElement.srcObject = e.stream;
    }
    // The third way, not discarded, outgoing trigger, incoming not trigger......
    session.connection.ontrack = (e) = >{
      audioElement.srcObject = e.streams[0]}// The first three were abandoned and finally studied for a long time
    // The final way to shine
    const stream = new MediaStream();
    constreceivers = session.connection? .getReceivers();if (receivers) receivers.forEach((receiver) = > stream.addTrack(receiver.track));
		audioElement.srcObject = stream;
    // Play all of them at the end
    audioElement.play();
  });
}
Copy the code

Having determined how to play the sound, the next step is how to answer the phone

Pick up the phone, hang up

In the process, it should look something like this

ua.on('newRTCSession', (e) => {
  const { session } = e;
  session.answer();
}
Copy the code

But that’s definitely not going to work, because this event definitely needs to be bound to the button, so you need to move this event out, so you need a variable to hold the session object

function sip() {
  // ...
  let currentSession;
  ua.on('newRTCSession', (e) => {
    const { session } = e;
    currentSession = session;
  });
  // Package layer 1 call function
  const answer = (options) = > {
    try {
      if (currentSession) currentSession.answer(options);
      else console.error('RTCSession object is empty when connected');
    } catch (e) {
      console.warn('Failed to connect:', e.message); }};return {
    answer
  };
}
Copy the code

In the same way as making and hanging calls, you need to send the event back out, hang it on the element, and then wrap it and return it like this

return {
  call, / / make a phone call
  terminate, / / hang up
  answer, / / answer
};
Copy the code

One of the most important points in the encapsulation process is this. In fact, every session callback can use this to access the session object. This was forgotten after the arrow function was written too much and the rule of not using this in the function body was ignored

So you can pass in a whole callback directly from the outside, and they all have access to the Session object, so you can add your own custom callback, and then bind this with call

// Current status
export enum Originator {
  Local = 'local',
  Remote = 'remote',
  Idle = 'idle',}// Passable callback
export interfaceCallEventHandlers { connecting? (this: RTCSession, event: SessionConnectingEvent): void; accepted? (this: RTCSession, event: SessionAcceptedEvent): void; confirmed? (this: RTCSession, event: SessionConfirmedEvent, stream: MediaStream): void; ended? (this: RTCSession, event: SessionEndedEvent): void; failed? (this: RTCSession, event: SessionFailedEvent): void;
  // Customize the event when a new call comes innewCall? (this: RTCSession, originator: Originator): void;
  // Customize the event when a phone call is connectedconnected? (this: RTCSession, originator: Originator): void;
}
Copy the code

There is also a pit here, I have not found the official solution, that is, there is no event for the other party to answer the call after I make the call. This event corresponds to the connected event above me. My solution here is to write a websocket in the background to tell me whether to answer the call or not

conclusion

Using JsSIP does not need to deal with SIP signaling, nor do we need to learn WebRTC related knowledge, although I think its event form is not very good, TS type is not perfect, but it is a very good, very simple to use library

Personally think call this a set of technical difficulties of the main page or in the background, so I admire us the background here, my brain can’t imagine what happened to him completely device plugged into the hundreds of card, how to connect to Linux, and how to take a STUN and TURN the server, and how to write the service can speak to me, really to hide

I think maybe the SIP protocol should not be on the front end processing, should the server to another SIP protocol parsing middleware, front-end simply use WebRTC is better, it was supposed to feel this call is easy, the weekend to yourself to learn WebRTC write an online video demo, the result looks difficult gave up, How can you give up today and do it next week


Welcome to pay attention to my public number ~


Reference link: developer.mozilla.org/zh-CN/docs/…