Skip to content

Commit

Permalink
Feature/chrome ext mv3 (#262)
Browse files Browse the repository at this point in the history
* VBLOCKS-2929 VBLOCKS-2927 Manual reconnection API

* Address PR comment.

Co-authored-by: Kevin Choy <[email protected]>

* Move connectToken to options

* Putting back docstring

* Validating non optional params

* VBLOCKS-2928 Chrome Extension MV3 App

* Removing default identity

* VBLOCKS-2832 Send reconnect flag to insights

* Changelog

* Lint

* For RC

* PR Review

* RC Prep: Sync lock file

* 2.11.0-rc1

* 2.11.0-dev

* Improve doc

* Tests, Docs, Decode/Encode Special Chars

* PR review

---------

Co-authored-by: Kevin Choy <[email protected]>
Co-authored-by: twilio-vblocks-ci <[email protected]>
  • Loading branch information
3 people authored May 1, 2024
1 parent 3a35fa6 commit c5f3af7
Show file tree
Hide file tree
Showing 42 changed files with 2,081 additions and 354 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ commands:
- build
- run:
name: Running build checks
command: npm run test:es5 && npm run test:esm && npm run test:typecheck && npm run lint && npm run test:unit
command: npm run test:build && npm run test:unit
- store_artifacts:
path: coverage
destination: coverage
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ coverage
dist
es5
esm
extension/token.js
node_modules
docs
lib/twilio/constants.ts
Expand Down
76 changes: 0 additions & 76 deletions BUILD.md

This file was deleted.

46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
:warning: **Important**: If you are upgrading to version 2.3.0 or later and have firewall rules or network configuration that blocks any unknown traffic by default, you need to update your configuration to allow connections to the new DNS names and IP addresses. Please refer to this [changelog](#230-january-23-2023) for more details.

2.11.0 (In Progress)
====================

New Features
------------

### Chrome Extensions Manifest V3 Support

In Manifest V2, [Chrome Extensions](https://developer.chrome.com/docs/extensions) have the ability to run the Voice JS SDK in the background when making calls. But with the introduction of [Manifest V3](https://developer.chrome.com/docs/extensions/develop/migrate/what-is-mv3), running the Voice JS SDK in the background can only be achieved through service workers. Service workers don't have access to certain features such as DOM, getUserMedia, and audio playback, making it impossible to make calls with previous versions of the SDK.

With this new release, the SDK can now run in a service worker context to listen for incoming calls or initiate outgoing calls. When the call object is created, it can be forwarded to an [offscreen document](https://developer.chrome.com/docs/extensions/reference/api/offscreen) where the SDK has access to all the necessary APIs to fully establish the call. Check our [example](extension) to see how this works.

### Client side incoming call forwarding and better support for simultaneous calls

Prior versions of the SDK support simultaneous outgoing and incoming calls using different [identities](https://www.twilio.com/docs/iam/access-tokens#step-3-generate-token). If an incoming call comes in and the `Device` with the same identity is busy, the active call needs to be disconnected before accepting the incoming call. With this new release of the SDK, multiple incoming calls for the same identity can now be accepted, muted, or put on hold, without disconnecting any existing active calls. This can be achieved by forwarding the incoming call to a different `Device` instance. See the following new APIs and example for more details.

#### New APIs
- [Call.connectToken](https://twilio.github.io/twilio-voice.js/classes/voice.call.html#connecttoken)
- [ConnectOptions.connectToken](https://twilio.github.io/twilio-voice.js/interfaces/voice.device.connectoptions.html#connecttoken)

#### Example

```js
// Create a Device instance that handles receiving of all incoming calls for the same identity.
const receiverDevice = new Device(token, options);
await receiverDevice.register();

receiverDevice.on('incoming', (call) => {
// Forward this call to a new Device instance using the call.connectToken string.
forwardCall(call.connectToken);
});

// The forwardCall function may look something like the following.
async function forwardCall(connectToken) {
// For every incoming call, we create a new Device instance which we can
// interact with, without affecting other calls.
// IMPORTANT: The token for this new device needs to have the same identity
// as the token used in the receiverDevice.
const device = new Device(token, options);
const call = await device.connect({ connectToken });

// Destroy the device after the call is completed
call.on('disconnect', () => device.destroy());
}
```

2.10.2 (February 14, 2024)
==========================

Expand Down
10 changes: 10 additions & 0 deletions extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## Twilio Voice JS SDK Chrome Extension Manifest V3 Example Application
This project demonstrates how to use the Twilio Voice JS SDK in a Chrome Extension Manifest V3 (MV3) application. If you are not familiar with MV3, please check out the official [documentation](https://developer.chrome.com/docs/extensions/develop/migrate/what-is-mv3) for more details.

This project has a server component that provides the Voice Access Token, and the Chrome Extension example app which can be loaded on the Chrome browser. See the diagram below for more details on the call flow.

### Incoming Call
![incoming-sequence](https://github.com/twilio/twilio-voice.js/assets/22135968/a5e3d03a-53db-49b9-948b-692cd568860d)

### Outgoing Call
![outgoing-sequence](https://github.com/twilio/twilio-voice.js/assets/22135968/31687569-a106-497d-b7b2-82e4a71e2f7f)
Binary file added extension/app/icons/active.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added extension/app/icons/default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions extension/app/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "Twilio Dialer",
"version": "1.0",
"description": "Shows how to implement a Twilio Dialer in a Chrome Extension MV3 using Twilio Voice JS SDK.",
"manifest_version": 3,
"action": {
"default_popup": "popup/popup.html",
"default_icon": "icons/default.png"
},
"background": {
"service_worker": "worker.js",
"type": "module"
},
"permissions": [
"offscreen"
]
}
Binary file added extension/app/offscreen/incoming.mp3
Binary file not shown.
8 changes: 8 additions & 0 deletions extension/app/offscreen/offscreen.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<head>
<title>Twilio Dialer (Offscreen)</title>
</head>
<body>
<script src="../twilio.js"></script>
<script src="offscreen.js"></script>
</body>
76 changes: 76 additions & 0 deletions extension/app/offscreen/offscreen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@

let device;
let call;
let incomingCallConnectToken;
let incomingSound;

async function start() {
console.log('Starting offscreen');
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
if (request.type === 'hangup' && sender.url.includes('popup')) {
device.disconnectAll();
} else if (request.type === 'connect') {
setupDevice(request.identity, request.recepient, request.connectToken);
} else if (request.type === 'connect-incoming') {
stopIncomingSound();
call = await device.connect({ connectToken: incomingCallConnectToken });
setupCallHandlers(call);
}
});
}

async function setupDevice(identity, recepient, connectToken) {
const { token } = await getJson('http://127.0.0.1:3030/token?identity=' + identity);

device = new Twilio.Device(token, { logLevel: 1 });

// The recepient parameter is provided to the offscreen document
// when making an outgoing call. If it exists, we initiate the call right away.
if (recepient) {
call = await device.connect({ params: { recepient } });
setupCallHandlers(call);
} else if(connectToken) {
incomingCallConnectToken = connectToken;
playIncomingSound();
}
}

function setupCallHandlers(call) {
call.on('accept', () => chrome.runtime.sendMessage({ type: 'accept' }));
call.on('disconnect', () => chrome.runtime.sendMessage({ type: 'disconnect' }));
call.on('cancel', () => chrome.runtime.sendMessage({ type: 'cancel' }));
call.on('reject', () => chrome.runtime.sendMessage({ type: 'reject' }));
}

function getJson(url) {
return new Promise(resolve => {
const xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
resolve(JSON.parse(this.responseText));
}
};
xmlhttp.open('GET', url, true);
xmlhttp.send();
});
}

async function playIncomingSound() {
if (!incomingSound) {
incomingSound = await new Promise(resolve => {
const audio = new Audio('incoming.mp3');
audio.loop = true;
audio.addEventListener('canplaythrough', () => resolve(audio));
});
}
incomingSound.play();
}

async function stopIncomingSound() {
if (incomingSound) {
incomingSound.pause();
incomingSound.currentTime = 0;
}
}

addEventListener('load', start);
33 changes: 33 additions & 0 deletions extension/app/popup/popup.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
body {
margin: 0px;
padding: 0px;
background: linear-gradient(to right, #3b679e 0%,#2b88d9 50%,#207cca 100%,#7db9e8 100%,#207cca 101%);
font-family: Arial, Helvetica, sans-serif;
}
#root {
padding: 25px;
}
#recepient {
display: none;
}
#status {
font-size: 20px;
margin: 0;
white-space: nowrap;
}
input, button {
font-size: 25px;
}
button {
border-radius: 0%;
min-width: 105px;
margin-top: 10px;
display: none;
white-space: nowrap;
}
pre {
margin: 5px;
background-color: white;
max-height: 200px;
overflow: auto;
}
18 changes: 18 additions & 0 deletions extension/app/popup/popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<head>
<title>Twilio Dialer</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div id="root">
<p id="status"></p>
<input type="text" placeholder="Recepient" id="recepient" value="">
<button id="init">Init Device</button>
<button id="call">Call</button>
<button id="hangup">Hangup</button>
<button id="accept">Accept</button>
<button id="reject">Reject</button>
</div>
<pre id="log"></pre>
<script src="popup.js"></script>
</body>
69 changes: 69 additions & 0 deletions extension/app/popup/popup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
let initBtn;
let callBtn;
let hangupBtn;
let acceptBtn;
let rejectBtn;
let recepientEl;
let statusEl;

function start() {
recepientEl = document.querySelector('#recepient');
statusEl = document.querySelector('#status');
initBtn = document.querySelector('#init');
callBtn = document.querySelector('#call');
hangupBtn = document.querySelector('#hangup');
acceptBtn = document.querySelector('#accept');
rejectBtn = document.querySelector('#reject');

setupButtonHandlers();
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === 'setstate') {
setStatus(request.state.status);
}
});
chrome.runtime.sendMessage({ type: 'getstate' }, ({ status }) => {
setStatus(status);
});

// Connect to the service worker. This allows the service worker
// to detect whether the popup is open or not.
chrome.runtime.connect();
}

function setStatus(status) {
if (status === 'pending') {
showButtons('init');
} else if (status === 'idle') {
showButtons('call');
} else if (status === 'incoming') {
showButtons('accept', 'reject');
} else if (status === 'inprogress') {
showButtons('hangup');
}
recepientEl.style.display = status === 'idle' ? 'block' : 'none';
statusEl.innerHTML = `Status: ${status}`;
}

function setupButtonHandlers() {
initBtn.onclick = () => chrome.runtime.sendMessage({ type: 'init' });
callBtn.onclick = () => recepientEl.value && chrome.runtime.sendMessage({ type: 'call', recepient: recepientEl.value });
hangupBtn.onclick = () => chrome.runtime.sendMessage({ type: 'hangup' });
acceptBtn.onclick = () => chrome.runtime.sendMessage({ type: 'accept' });
rejectBtn.onclick = () => chrome.runtime.sendMessage({ type: 'reject' });
}

function showButtons(...buttonsToShow) {
document.querySelectorAll('button').forEach(el => {
if (buttonsToShow.includes(el.id)) {
el.style.display = 'inline-block';
} else {
el.style.display = 'none';
}
});
}

function log(msg) {
document.querySelector('#log').innerHTML += msg + '\n';
}

start();
1 change: 1 addition & 0 deletions extension/app/twilio.js
Loading

0 comments on commit c5f3af7

Please sign in to comment.