# Web

## I. Cài đặt <a href="#i-cai-dat" id="i-cai-dat"></a>

* Tải SIP.js tại đây: <https://github.com/onsip/SIP.js/releases> hoặc nếu project có sử dụng npm thì dùng `npm install sip.js`
* Phiên bản mới nhất hiện tại: **0.20.0**
* Document SIP.js: <https://github.com/onsip/SIP.js/blob/master/docs/README.md>

## II. Đăng nhập máy nhánh (extension) <a href="#ii-dang-nhap-may-nhanh-extension" id="ii-dang-nhap-may-nhanh-extension"></a>

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

{% code overflow="wrap" %}

```javascript
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;
```

{% endcode %}

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

{% code overflow="wrap" %}

```javascript
// 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();

}
```

{% endcode %}

## III. Đăng xuất máy nhánh (extension) <a href="#iii-dang-xuat-may-nhanh-extension" id="iii-dang-xuat-may-nhanh-extension"></a>

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

## IV. Thực hiện cuộc gọi (outgoing call) <a href="#iv-thuc-hien-cuoc-goi-outgoing-call" id="iv-thuc-hien-cuoc-goi-outgoing-call"></a>

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

{% code overflow="wrap" %}

```javascript
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);
}
```

{% endcode %}

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

{% code overflow="wrap" %}

```javascript
// 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}
    }
  };
}
```

{% endcode %}

## V. Nhận cuộc gọi (incoming call) <a href="#v-nhan-cuoc-goi-incoming-call" id="v-nhan-cuoc-goi-incoming-call"></a>

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

{% code overflow="wrap" %}

```javascript
**// 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).
  }

}
```

{% endcode %}

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

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

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

```javascript
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:

{% code overflow="wrap" %}

```javascript
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();
}
```

{% endcode %}

***Từ chối cuộc gọi***

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

## VI. **Xử lý cuộc gọi, lấy remoteMediaStream** <a href="#vi-xu-ly-cuoc-goi-lay-remotemediastream" id="vi-xu-ly-cuoc-goi-lay-remotemediastream"></a>

{% code overflow="wrap" %}

```javascript
_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));
    }
  }
}
```

{% endcode %}

* Giao diện của bạn phải có các tag `<audio>` và `<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ụ:

{% code overflow="wrap" %}

```html
<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>
```

{% endcode %}

## VII. L**ắng nghe các State của cuộc gọi** <a href="#vii-lang-nghe-cac-state-cua-cuoc-goi" id="vii-lang-nghe-cac-state-cua-cuoc-goi"></a>

{% code overflow="wrap" %}

```javascript
_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:
    }
  });
}
```

{% endcode %}

## VIII. Chủ động kết thúc cuộc gọi <a href="#viii-chu-dong-ket-thuc-cuoc-goi" id="viii-chu-dong-ket-thuc-cuoc-goi"></a>

```javascript
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 <a href="#ix-giu-may" id="ix-giu-may"></a>

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

<br>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.etelecom.vn/tich-hop-api/tong-dai/voip-sdk/web.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
