Web

I. Cài đặt

II. Đăng nhập máy nhánh (extension)

Khai báo các biến global và import các class sau:

import {
  Invitation,
  InvitationAcceptOptions,
  Inviter,
  InviterInviteOptions,
  Registerer,
  Session,
  SessionState,
  UserAgent,
  UserAgentOptions,
  Web
} from "sip.js";
import {IncomingResponse} from "sip.js/lib/core";

userAgent: UserAgent;

// registerer để quản lý các thao tác đăng nhập, đăng xuất máy nhánh.
registerer: Registerer;

// incomingInvitation để quản lý các sự kiện liên quan đến cuộc gọi đến (incoming call)
incomingInvitation: Invitation;

// outgoingInviter để quản lý các sự kiện liên quan đến cuộc gọi đi (outgoing call)
outgoingInviter: Inviter;

// khi một cuộc gọi bất kì diễn ra, 1 session được tạo ra, biến này dùng để theo dõi các trạng thái của cuộc gọi cho đến khi cuộc gọi kết thúc.
session: Session;

Khai báo UserAgent và Registerer để đăng nhập máy nhánh:

// Giải thích:
// username: extension_number (ví dụ: 1234, 5678, ...)
// password: extension_password (ví dụ: abcDefg123, xzyqerTT11, ...)
// domain: tenant_domain (ví dụ: voip.example.com, ...)
async login(username: string, password: string, domain: string) {

  const _uri = UserAgent.makeURI(`sip:${username}@${tenant_domain`);
  if (!_uri) { return; }

  const _userAgentOptions: UserAgentOptions = {
    uri: _uri,
    authorizationUsername: username,
    authorizationPassword: password,
    transportOptions: {
      server: "wss://sip.etelecom.vn:10443",
    },
    logLevel: "debug",
    displayName: username,
    delegate: {
      // chứa các sự kiện như có cuộc gọi đến, có cuộc gọi chuyển tiếp, ...
      // ví dụ ở mục V sẽ nói tới nhận cuộc gọi đến, phần đó sẽ được để trong này
    }
  };
  this.userAgent = new UserAgent(_userAgentOptions);

  this.registerer = new Registerer(this.userAgent, {});

  await this.userAgent?.start();

  await this.registerer?.register();

}

III. Đăng xuất máy nhánh (extension)

async logout() {
  await this.registerer?.unregister();
  await this.userAgent?.stop();
}

IV. Thực hiện cuộc gọi (outgoing call)

Để thực hiện cuộc gọi:

async call(phoneNumber: string) {
  const target = UserAgent.makeURI(`sip:${phone}@${tenant_domain}`);
  if (!target) { return; }

  const inviter = new Inviter(this.userAgent, target, {});

  this.outgoingInviter = inviter;
  this.session = inviter;

  **// lắng nghe các State của cuộc gọi: bắt đầu, kết thúc, ... Sẽ nói ở mục VII.** 
  this._sessionStateListener();

  const _inviterInviteOptions: InviterInviteOptions = {
    requestDelegate: {
      // Khi cuộc gọi bắt đầu thì sự kiện onProgress này xảy ra.
      onProgress: (response: IncomingResponse) => {
        if (response.message.headers['X-Session-Id']?.length) {
          // TODO: lấy X-Session-Id ra để phục vụ cho việc lấy Call Logs (nếu cần).
        }
      },
      // Khi cuộc gọi được người nhận chấp nhận thì sự kiện onAccept này xảy ra.
      onAccept: (response: IncomingResponse) => {
        this.session = inviter;

        **// Xử lý cuộc gọi, lấy remoteMediaStream (audio/video) của khách hàng
        // để có thể nghe và nói chuyện được với khách hàng.
        // Sẽ nói ở mục VI.**
        this._processCalls();
      }
    }
  };

  await this.outgoingInviter.invite(_inviterInviteOptions);
}

Cuộc gọi video (Video Call), tiếp tục là hàm call() bên trên:

// bạn có thể tuỳ chỉnh những thông số này theo nếu cần thiết.
const videoConstraints: MediaTrackConstraints = {
  advanced: [
    {
      width: 480,
      height: 640,
      echoCancellation: true,
      frameRate: 20,
    }
  ]
};

async call(phoneNumber: string) {
  
  [...]
  **// lắng nghe các State của cuộc gọi: bắt đầu, kết thúc, ... Sẽ nói ở mục VII.** 
  this._sessionStateListener();

  const constraints: MediaStreamConstraints = {
    video: videoConstraints,
    audio: true
  };

  **// chuẩn bị webcam để tiến hành video call.**
  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    // tìm Local Video Element (Local là phía user, phân biệt với Remote là phía người đàm thoại với User)
    const localVideo = document.getElementById("localVideo") as HTMLVideoElement;

    // sau đó gán MediaStream làm source của Video Element đó
    localVideo.srcObject = stream;

    // gọi hàm play() để phát video.
    localVideo.play().catch(err => console.error("ERROR in Streaming Local Video", err));
  });

  const _inviterInviteOptions: InviterInviteOptions = {
    requestDelegate: {
      [...]
      sessionDescriptionHandlerOptions: {constraints}
    }
  };
}

V. Nhận cuộc gọi (incoming call)

Thêm giá trị này vào field delegate của UserAgentOptions ở mục II, bước 2.

**// khi có cuộc gọi đến, sự kiện onInvite này sẽ xảy ra**
onInvite: (invitation: Invitation) => {

  this.incomingInvitation = invitation;
  this.session = invitation;
  **// lắng nghe các State của cuộc gọi: bắt đầu, kết thúc, ... Sẽ nói ở mục VII.** 
  this._sessionStateListener();

  if (this.incomingInvitation.request.headers['X-Session-Id']?.length) {
    // TODO: lấy X-Session-Id ra để phục vụ cho việc lấy Call Logs (nếu cần).
  }

}
  • Nếu là cuộc gọi video thì check biến incomingInvitation

this.incomingInvitation.body.includes("label:video_label")

Chấp nhận cuộc gọi

async answer() {
  await this.incomingInvitation.accept();
  **// Xử lý cuộc gọi, lấy remoteMediaStream (audio/video) của khách hàng
  // để có thể nghe và nói chuyện được với khách hàng.
  // Sẽ nói ở mục VI.**
  this._processCalls();
}
  • Nếu là cuộc gọi video thì cũng với hàm answer() bên trên, ta có thể hiện thực như sau:

async answer() {
  const constraints: MediaStreamConstraints = {
    video: videoConstraints,
    audio: true
  };

  **// chuẩn bị webcam để tiến hành video call.**
  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    // tìm Local Video Element (Local là phía user, phân biệt với Remote là phía người đàm thoại với User)
    const localVideo = document.getElementById("localVideo") as HTMLVideoElement;

    // sau đó gán MediaStream làm source của Video Element đó
    localVideo.srcObject = stream;

    // gọi hàm play() để phát video.
    localVideo.play().catch(err => console.error("ERROR in Streaming Local Video", err));
  });

  await this.incomingInvitation.accept({
    sessionDescriptionHandlerOptions: {constraints}
  });

  this._processCalls();
}

Từ chối cuộc gọi

async reject() {
  await this.incomingInvitation.reject();
}

VI. Xử lý cuộc gọi, lấy remoteMediaStream

_processCalls() {
  const sessionDescriptionHandler = this.session.sessionDescriptionHandler;
  if (sessionDescriptionHandler && sessionDescriptionHandler instanceof Web.SessionDescriptionHandler) {
    if (**cuộc gọi là video**) {
      // tìm Remote Video Element trên HTML DOM
      const remoteVideo = document.getElementById("remoteVideo") as HTMLVideoElement;
  
      // sau đó gán MediaStream làm source của Video Element đó
      remoteVideo.srcObject = sessionDescriptionHandler.remoteMediaStream;
  
      // gọi hàm play() để phát video.
      remoteVideo.play().catch(err => console.error("ERROR in Streaming Remote Video", err));
    } else {
      // tìm Audio Element trên HTML DOM
      const remoteAudio = document.getElementById("remote") as HTMLAudioElement;
  
      // sau đó gán MediaStream làm source của Audio Element đó
      remoteAudio.srcObject = sessionDescriptionHandler.remoteMediaStream;
  
      // gọi hàm play() để phát âm thanh.
      remoteAudio.play().catch(err => console.error("ERROR in Streaming Remote Audio", err));
    }
  }
}
  • Giao diện của bạn phải có các tag <audio><video> để hiển thị video và phát audio khi nghe gọi. Với local là phía user và remote là phía người đàm thoại với user.

  • Ví dụ:

<video id="remoteVideo" autoplay>
</video>
<video id="localVideo" autoplay>
</video>
<audio style="display: none" id="remoteAudio"></audio>
<!--localAudio có thể được dùng để chứa audio về ring back tone, nhạc chuông báo cuộc gọi đến...-->
<audio style="display: none" id="localAudio" loop></audio>

VII. Lắng nghe các State của cuộc gọi

_sessionStateListener() {
  this.session.stateChange.addListener((state: SessionState) => {
    switch (state) {
      case SessionState.Initial:
      // Cuộc gọi vừa được khởi tạo.

      case SessionState.Establishing:
      // Cuộc gọi vừa được chấp nhận.

      case SessionState.Established:
      // Cuộc gọi bắt đầu diễn ra.

      case SessionState.Terminating:
      // Cuộc gọi vừa được kết thúc.

      case SessionState.Terminated:
        // Cuộc gọi hoàn toàn kết thúc

      default:
    }
  });
}

VIII. Chủ động kết thúc cuộc gọi

async hangup() {
  if (**Cuộc gọi là cuộc gọi đi và mới được khởi tạo, chưa được chấp nhận**) {
    await this.outgoingInviter.cancel();
  } else {
    await this.session?.bye();
  }
}

IX. Giữ máy

async hold(isHold: boolean) {
  const _holdOptions: Web.SessionDescriptionHandlerOptions = {
    hold: isHold
  };
  
  this.session.sessionDescriptionHandlerOptionsReInvite = _holdOptions;
  
  this.session.invite().then();
}

Last updated