Développement Web

Construire un softphone navigateur avec JsSIP et Asterisk

12 min de lecture
Guide principal : Téléphonie IP avancée : 3CX, Issabel et softphones WebRTC. Ce tutoriel suppose un serveur Asterisk déjà préparé pour WebRTC ; sinon, suivez d’abord activer WebRTC sur Asterisk.

Une fois Asterisk prêt à accueillir des clients WebRTC, il reste à construire le téléphone lui-même : une page web capable de s’enregistrer, de passer et de recevoir des appels. La bibliothèque JsSIP rend cette tâche étonnamment accessible. Écrite en JavaScript, elle gère toute la complexité du protocole SIP et de WebRTC à votre place, vous laissant écrire un softphone fonctionnel en quelques dizaines de lignes. Ce tutoriel construit ce softphone pas à pas, en expliquant chaque concept avant chaque ligne de code, pour que vous compreniez ce que vous écrivez plutôt que de le recopier.

Prérequis

  • Un serveur Asterisk configuré pour WebRTC, avec un endpoint dont vous connaissez l’identifiant et le mot de passe.
  • L’URL du WebSocket sécurisé du serveur, de la forme wss://votre-domaine:8089/ws.
  • Un serveur web pour servir la page en HTTPS (les navigateurs exigent HTTPS pour accéder au microphone).
  • Des bases de HTML et JavaScript.
  • Niveau : avancé. Temps estimé : 1 heure.

Comprendre l’architecture de JsSIP

Avant de coder, situons les pièces. JsSIP s’organise autour d’un objet central, le User Agent (UA), qui représente votre téléphone : c’est lui qui s’enregistre auprès du serveur, émet les appels et reçoit les appels entrants. Le UA s’appuie sur un transport — ici un WebSocket sécurisé — pour dialoguer avec Asterisk. Chaque appel, entrant ou sortant, est représenté par une session RTC, un objet qui porte l’état de la conversation et donne accès au flux audio.

La logique de JsSIP est entièrement événementielle : on ne « demande » pas si un appel arrive, on s’abonne à des événements (« enregistré », « nouvel appel », « appel terminé ») et le code réagit quand ils surviennent. Cette approche, naturelle en JavaScript, structure tout le tutoriel : à chaque étape, on branche un gestionnaire sur un événement précis.

Étape 1 — Inclure la bibliothèque

La première chose à faire est de charger JsSIP dans la page. Le plus simple pour débuter est de l’inclure depuis un réseau de distribution de contenu (CDN), sans rien installer localement. Pour un projet sérieux, on l’installera plutôt via le gestionnaire de paquets npm afin de figer la version.

<script src="https://cdnjs.cloudflare.com/ajax/libs/jssip/3.10.1/jssip.min.js"></script>

Cette balise charge JsSIP et expose l’objet global JsSIP dans la page. Vérifiez dans la console du navigateur que JsSIP.version renvoie bien un numéro : c’est le signe que la bibliothèque est chargée et prête. Si vous obtenez une erreur, c’est que l’inclusion a échoué — vérifiez l’URL ou votre connexion.

Étape 2 — Préparer le squelette HTML

Un softphone a besoin d’un élément pour jouer le son de l’interlocuteur et de quelques boutons pour agir. L’élément central est une balise <audio> : c’est elle qui diffusera la voix reçue. On y ajoute un champ pour saisir le numéro à appeler et des boutons d’action.

<audio id="remoteAudio" autoplay></audio>
<input id="target" placeholder="Numéro à appeler">
<button id="callBtn">Appeler</button>
<button id="hangupBtn">Raccrocher</button>

L’attribut autoplay sur la balise audio permet au son de démarrer dès qu’un flux y est attaché, sans clic supplémentaire. Les deux boutons et le champ de saisie suffisent pour un softphone minimal ; on les reliera au code JsSIP dans les étapes suivantes. Cette sobriété est volontaire : on construit d’abord le fonctionnel, l’habillage viendra ensuite.

Étape 3 — Créer le User Agent

Le cœur du softphone est l’objet UA. On le configure avec trois informations : le transport WebSocket, l’identité SIP de l’utilisateur, et son mot de passe — exactement les identifiants de l’endpoint WebRTC créé côté Asterisk.

const socket = new JsSIP.WebSocketInterface('wss://votre-domaine:8089/ws');
const configuration = {
  sockets: [ socket ],
  uri: 'sip:webrtc_client@votre-domaine',
  password: 'un_mot_de_passe_robuste'
};
const ua = new JsSIP.UA(configuration);
ua.start();

On crée d’abord l’interface WebSocket pointant vers le port sécurisé d’Asterisk. La configuration relie ce transport à l’URI SIP de l’utilisateur et à son mot de passe. L’appel à ua.start() démarre le User Agent : il ouvre la connexion WebSocket et tente de s’enregistrer auprès du serveur. À ce stade, rien n’est visible à l’écran, mais en coulisses le téléphone s’annonce à Asterisk.

Étape 4 — Réagir à l’enregistrement

Comment savoir si l’enregistrement a réussi ? En s’abonnant aux événements correspondants. JsSIP émet registered en cas de succès et registrationFailed en cas d’échec. On branche un gestionnaire sur chacun pour informer l’utilisateur.

ua.on('registered', () => {
  console.log('Enregistré : prêt à appeler');
});
ua.on('registrationFailed', (e) => {
  console.error('Échec de l\'enregistrement', e.cause);
});

Si vous voyez « Enregistré » dans la console, votre softphone est joignable et peut appeler. Si l’enregistrement échoue, la propriété cause de l’événement indique pourquoi — le plus souvent des identifiants erronés ou un WebSocket inaccessible. C’est le premier point de contrôle : tant que l’enregistrement n’aboutit pas, inutile de tester les appels.

Étape 5 — Passer un appel sortant

Pour appeler, on utilise la méthode ua.call() en lui passant la cible et des options. L’option déterminante est mediaConstraints : pour un softphone audio, on demande l’audio mais pas la vidéo. C’est aussi à ce moment que le navigateur demandera l’autorisation d’accéder au microphone.

document.getElementById('callBtn').onclick = () => {
  const target = document.getElementById('target').value;
  const session = ua.call('sip:' + target + '@votre-domaine', {
    mediaConstraints: { audio: true, video: false }
  });
  window.currentSession = session;
};

Au clic, on récupère le numéro saisi, on compose l’URI SIP correspondante, et on lance l’appel en n’autorisant que l’audio. On conserve la session dans une variable pour pouvoir raccrocher plus tard. Le navigateur affiche alors sa demande d’accès au microphone : sans cette autorisation, aucun son ne partira. C’est une exigence de sécurité des navigateurs, pas un bug.

Étape 6 — Diffuser la voix de l’interlocuteur

Passer l’appel ne suffit pas : il faut encore brancher le flux audio reçu sur la balise <audio>, sinon vous parlez mais n’entendez rien. JsSIP expose la connexion média de la session ; on y écoute l’arrivée du flux distant pour l’attacher à l’élément audio.

ua.on('newRTCSession', (data) => {
  const session = data.session;
  session.connection.addEventListener('track', (e) => {
    document.getElementById('remoteAudio').srcObject = e.streams[0];
  });
});

L’événement newRTCSession se déclenche pour chaque appel, entrant comme sortant. On y accède à la connexion WebRTC sous-jacente et on écoute l’événement track, émis quand le flux distant arrive. On attache alors ce flux à la propriété srcObject de la balise audio, et grâce à l’attribut autoplay, le son démarre immédiatement. C’est l’étape que les débutants oublient le plus souvent, d’où le classique « l’appel se connecte mais je n’entends rien ».

Étape 7 — Recevoir un appel entrant

Un vrai téléphone doit aussi sonner. L’événement newRTCSession permet de distinguer les appels entrants des sortants grâce à la propriété originator. Pour un appel entrant, on peut répondre automatiquement ou attendre une action de l’utilisateur.

ua.on('newRTCSession', (data) => {
  if (data.originator === 'remote') {
    const session = data.session;
    // répondre en audio seulement
    session.answer({ mediaConstraints: { audio: true, video: false } });
  }
});

Quand l’appel vient de l’extérieur (originator vaut remote), on appelle session.answer() avec les mêmes contraintes média que pour un appel sortant. Dans un softphone réel, on afficherait plutôt un bouton « Décrocher » que l’on relierait à cette méthode, pour laisser le choix à l’utilisateur. Le principe reste le même : répondre, c’est appeler answer() sur la session entrante.

Étape 8 — Raccrocher et tester

Le bouton « Raccrocher » termine la session en cours. On relie son clic à la méthode terminate() de la session conservée à l’étape 5.

document.getElementById('hangupBtn').onclick = () => {
  if (window.currentSession) window.currentSession.terminate();
};

Pour tester l’ensemble, servez la page depuis votre serveur web en HTTPS, ouvrez-la dans un navigateur, et observez la console : vous devez voir « Enregistré ». Composez ensuite le numéro d’une autre extension et cliquez sur « Appeler ». Après l’autorisation du microphone, l’appel doit s’établir et la voix circuler dans les deux sens. Si tout fonctionne, vous venez de construire un téléphone fonctionnel qui tient dans une page web.

Comprendre l’autorisation du microphone

Un point déroute beaucoup de développeurs : pourquoi le navigateur demande-t-il l’accès au microphone, et pourquoi seulement en HTTPS ? La réponse tient à la protection de la vie privée. Capter le micro d’un utilisateur est une opération sensible ; les navigateurs imposent donc deux garde-fous. D’abord, la page doit être servie sur une origine sécurisée — HTTPS avec un certificat valide, ou localhost en développement. Une page en HTTP simple n’a tout bonnement pas le droit de demander le micro, et l’appel échouera silencieusement. Ensuite, l’utilisateur doit accorder explicitement l’autorisation via la fenêtre du navigateur.

Concrètement, cela signifie que votre softphone JsSIP ne fonctionnera jamais sur une page de test ouverte en double-cliquant sur un fichier local : il faut un vrai serveur web servant la page en HTTPS. En développement, beaucoup utilisent un certificat local de confiance ou exposent leur poste via un tunnel sécurisé. En production, le certificat du domaine — le même que celui d’Asterisk, idéalement — règle la question. Gardez aussi à l’esprit que l’autorisation est mémorisée par origine : une fois accordée pour votre domaine, le navigateur ne la redemande plus à chaque appel, ce qui fluidifie l’expérience.

Fiabiliser le softphone en production

Le softphone minimal de ce tutoriel fonctionne, mais un usage réel demande quelques renforcements. Le premier concerne la reconnexion : un poste de travail passe en veille, le réseau Wi-Fi vacille, et le WebSocket se coupe. JsSIP tente de se reconnecter automatiquement, mais il est sage d’écouter les événements de connexion du transport pour informer l’utilisateur quand le téléphone est temporairement indisponible, plutôt que de le laisser croire qu’il est joignable alors qu’il ne l’est plus.

Le deuxième renforcement touche à la gestion des états d’appel. Un softphone professionnel affiche clairement si un appel sonne, est en cours, ou s’est terminé, et désactive le bouton « Appeler » pendant une communication. Ces retours visuels, branchés sur les événements progress, confirmed et ended de la session, évitent les doubles appels et les manipulations hasardeuses. Ils ne changent rien au fonctionnement interne, mais transforment un prototype en outil utilisable au quotidien.

Enfin, ne stockez jamais le mot de passe SIP en clair dans le code JavaScript livré au navigateur : c’est une fuite de sécurité majeure, car n’importe qui inspectant la page le lirait. En production, on récupère les identifiants depuis un point d’accès authentifié côté serveur, ou l’on génère des identifiants temporaires propres à chaque session. Le softphone reste alors pratique sans exposer les secrets qui permettraient à un tiers d’usurper l’extension et de passer des appels frauduleux — le même risque que celui traité dans les tutoriels de sécurisation des plateformes.

Erreurs fréquentes

Erreur Cause Solution
« Enregistré » ne s’affiche jamais URL WebSocket erronée ou identifiants faux Vérifier l’URL wss://...:8089/ws et les identifiants de l’endpoint
Appel connecté mais aucun son Flux distant non attaché à la balise audio Brancher l’événement track sur srcObject (Étape 6)
Le micro n’est pas demandé Page servie en HTTP au lieu de HTTPS Servir la page en HTTPS avec un certificat valide
Appel qui échoue entre réseaux Absence de serveur TURN Déployer coturn et le déclarer côté client
Erreur de certificat sur le WebSocket Certificat Asterisk non reconnu Utiliser un certificat reconnu correspondant au domaine

Tutoriels associés

Pour aller plus loin

Questions fréquentes

JsSIP ou SIP.js, lequel choisir ?
Les deux font le même travail. JsSIP est réputé pour sa simplicité d’accès et sa documentation directe ; SIP.js offre une API plus moderne et typée, appréciée dans les projets TypeScript. Pour un premier softphone, JsSIP est souvent le plus rapide à prendre en main.

Pourquoi mon appel n’a-t-il pas de son alors qu’il se connecte ?
Parce que le flux audio distant n’est pas attaché à la balise <audio>. C’est l’erreur la plus fréquente : il faut écouter l’événement track de la connexion et affecter le flux reçu à srcObject.

Peut-on faire de la vidéo avec JsSIP ?
Oui, en passant video: true dans les contraintes média et en attachant le flux à une balise <video>. Le principe est identique à l’audio, avec un élément d’affichage en plus.

Faut-il un serveur TURN pour ce softphone ?
En test local entre deux postes du même réseau, non. En production, dès que les utilisateurs sont sur des réseaux différents, un serveur TURN devient indispensable pour relayer le média quand la connexion directe échoue.

Service ITSkillsCenter

Application mobile Android et iOS

Création d'application mobile Android et iOS. À partir de 350 000 FCFA.

Démarrer mon projet
Publicité