Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 169 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,41 +188,183 @@ $ npm run listener

An easy way to test the plugin is to use a local policy. In this case we
designed a local policy for testing. It will allow every VMR with 2 or 6 digits
and will use the pins 1234 for the hosts and 4321 for the guests.
and will use the pins `1234` for the hosts and `4321` for the guests.

The process is as follows:

- We will use a local participant policy that adds a `callTag` to the
participant in the **main room**. To obtain it, we will follow this process:
1. Concatenate the following parameters:
- `local_alias`
- `vendor`
- `display_name`
2. Apply `pex_hash` to the concatenated string.
3. Take only the last 20 digits of the result.

- The plugin will retrieve the user's `callTag` and use it to generate the `pin`
for the **interpretation room**. To do so, we will follow this process:
1. Concatenate the following parameters:
- The previous `callTag`
- `role` (e.g., `interpreter` or `listener`)

- The service configuration policy will attempt to replicate the same `pin`
using the same process. It will choose the role `interpreter` for the `pin`
and `listener` for the `guest_pin`.

```mermaid
sequenceDiagram
participant Webapp3
participant Plugin
participant Infinity

Webapp3 ->> Infinity: Join main conference {pin: <vmr_pin>}
Infinity -->> Webapp3: Joined { call_tag: pexHash(local_alias + vendor + display_name).tail(20) }
Plugin ->> Infinity: Join interpretation {pin: pexHash(call_tag + role).tail(20)}
Infinity ->> Infinity: Check PIN:<br>pin: pexHash(call_tag + 'interpreter').tail(20),<br>guest_pin: pexHash(call_tag + 'listener').tail(20)
Infinity -->> Plugin: Joined
```

### Service configuration policy

```python
{
{% if (call_info.local_alias | pex_regex_replace('^(\d{2}|\d{6})$', '') == '') %}
{# Static PINs but we can define a function to generate a PIN per VMR #}
{% set pin = "1234" %}
{% set guest_pin = "4321" %}

{% set callTag = "" %}

{% if (call_info.local_alias | pex_regex_replace('^(\d{2})$', '') == '') %}
{# Main rooms for 2-digit VMRs #}

{
"action": "continue",
"result": {
"service_type": "conference",
"name": "{{call_info.local_alias}}",
"service_tag": "pexip-interpreter",
"description": "",
"call_tag": "",
"pin": "1234",
"guest_pin": "4321",
"guests_can_present": true,
"allow_guests": true,
"view": "four_mains_zero_pips",
"ivr_theme_name": "visitor_normal",
"locked": false,
"automatic_participants": []
}
{% elif service_config %}
{
"action" : "continue",
"result" : {{service_config | pex_to_json}}
"service_type": "conference",
"name": "{{call_info.local_alias}}",
"service_tag": "pexip-interpreter",
"pin": "{{pin}}",
"guest_pin": "{{guest_pin}}",
"guests_can_present": true,
"allow_guests": true,
"view": "four_mains_zero_pips"
}
{% else %}
{
"action" : "reject",
"result" : {}
}

{% elif (call_info.local_alias | pex_regex_replace('^(\d{6})$', '') == '') %}
{# Interpretation rooms for 6-digit VMRs #}

{% set callTag = ((call_info.local_alias | pex_regex_replace('^(\d{2})(\d{4})$', '\\1') ) + call_info.vendor + (call_info.remote_display_name | pex_regex_replace('^(.*)\ -\ (Interpreter|Listener)$', '\\1') )) | pex_hash | pex_tail(20) %}

{% set pin = (callTag + "interpreter") | pex_hash | pex_tail(20) %}
{% set guest_pin = (callTag + "listener") | pex_hash | pex_tail(20) %}

{
"action": "continue",
"result": {
"service_type": "conference",
"name": "{{call_info.local_alias}}",
"service_tag": "pexip-interpreter",
"pin": "{{pin}}",
"guest_pin": "{{guest_pin}}",
"allow_guests": true
}
{% endif %}
}
}

{% elif service_config %}

{
"action" : "continue",
"result" : {{service_config | pex_to_json}}
}

{% else %}

{
"action" : "reject",
"result" : {}
}

{% endif %}
```

### Participant policy

```python
{% set callTag = "" %}

{# Remove the Webapp3 suffix from the vendor string, e.g. " Webapp3/11.0.0+c29a9d064" #}
{% set vendor = call_info.vendor | pex_regex_replace(" Webapp3.*$", "") %}

{% if (call_info.local_alias | pex_regex_replace('^(\d{2})$', '') == '') %}
{% set callTag = (call_info.local_alias + vendor + call_info.remote_display_name) | pex_hash | pex_tail(20) %}

{
"status": "success",
"action": "continue",
"result": {
"call_tag": "{{callTag}}"
}
}

{% else %}

{
"status": "success",
"action": "continue",
"result": {}
}

{% endif %}
```

### Join with a SIP device to a interpretation room (testing only)

To join to an interpretation room through a SIP device you need to follow these
steps:

#### Create a new Call Routing Rule

- Click on `Services > Call Routing`.
- Click on `Add Call Routing Rule`.
- Define the following parameters (leave the rest as default):
- **Name:** Interpretation SIP Join
- **Priority:** 1
- **Destination alias regex match** .\*
- Click on `Save`.

#### Create a VMR for interpretation

- Click on `Services > Virtual Meeting Rooms`.
- Click on `Add Virtual Meeting Room`.
- Define the following parameters (leave the rest as default):
- **Name:** French room for 01
- **Host PIN:** 5678
- **Allow Guests:** true
- **Guest PIN:** 8765
- **Alias:** 010033
- **Advanced options**:
- **Guest can present:** false
- **Enable chat:** false
- **Conference capabilities:** Audio-only
- Click on `Save`.

#### Install a SIP softphone in your computer

- You need to install a SIP softphone in your computer. You can use
[Zoiper](https://www.zoiper.com/en/voip-softphone/download/current) or any
other softphone that you like.

- Configure the SIP softphone with a SIP account that can reach your Infinity
deployment.

- Make a call to the VMR created in the previous step (e.g.
010033@192.168.1.101). Use the dialpad to enter the PIN for the host or guest.

- In this case we will use the static VMR created before, so the host PIN is
`5678` and the guest PIN is `8765`. The reason is that the `local_alias` is
`sip:010033` and it doesn't match the regex for 2 or 6 digit VMRs from the
local policy.

## Build for production

To create a package you need to install first all the dependencies:
Expand Down
83 changes: 50 additions & 33 deletions src/InterpretationContext/InterpretationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { getMainConferenceAlias } from '../conference'
import { MainRoom } from '../main-room'
import { setButtonActive } from '../button'
import { logger } from '../logger'
import { capitalizeFirstLetter, pexHash } from '../utils'

const batchScheduleTimeoutMS = 500
const batchBufferSize = 10
Expand Down Expand Up @@ -84,8 +85,7 @@ export const InterpretationContextProvider = (props: {
infinityClient = createInfinityClient(clientSignals, callSignals)

const username = getUser().displayName ?? getUser().uuid
let roleTag = ''
let callType: ClientCallType = ClientCallType.Audio
const roleTag = capitalizeFirstLetter(state.role)

if (!state.connected) {
dispatch({
Expand All @@ -96,45 +96,28 @@ export const InterpretationContextProvider = (props: {
})
}

const { Audio, AudioSendOnly, AudioRecvOnly } = ClientCallType
if (state.role === Role.Interpreter) {
roleTag = 'Interpreter'
const callType = getCallType(state.role)

let mediaStream: MediaStream | undefined = undefined
if (callType !== ClientCallType.AudioRecvOnly) {
const constraints = MainRoom.getMediaConstraints()
mediaStream = await getMediaStream(constraints)
const { interpreter } = config
let shouldSendReceive = false
if (interpreter != null) {
const { allowChangeDirection } = interpreter
shouldSendReceive = allowChangeDirection
}
if (shouldSendReceive) {
callType = Audio
} else {
callType = AudioSendOnly
}
} else {
roleTag = 'Listener'
mediaStream = undefined
const { listener } = config
let shouldSendReceive = false
if (listener != null) {
const { speakToInterpretationRoom } = listener
shouldSendReceive = speakToInterpretationRoom
}
if (shouldSendReceive) {
const constraints = MainRoom.getMediaConstraints()
mediaStream = await getMediaStream(constraints)
callType = Audio
} else {
callType = AudioRecvOnly
}
}

const displayName = `${username} - ${roleTag}`

try {
const conferenceAlias = getMainConferenceAlias() + language.code
const bandwidth = 0

const { rawData } = getUser()
const { call_tag: callTag } = rawData

if (callTag != null) {
const maxSize = 20
const input = `${callTag}${state.role}`
pin = (await pexHash(input))?.slice(-maxSize) ?? pin
}

if (pin != null) {
await infinityClient.call({
conferenceAlias,
Expand Down Expand Up @@ -261,6 +244,40 @@ export const InterpretationContextProvider = (props: {
})
}

const getCallType = (role: Role): ClientCallType => {
const { Audio, AudioSendOnly, AudioRecvOnly } = ClientCallType

let callType: ClientCallType = Audio

if (role === Role.Interpreter) {
const { interpreter } = config
let shouldSendReceive = false
if (interpreter != null) {
const { allowChangeDirection } = interpreter
shouldSendReceive = allowChangeDirection
}
if (shouldSendReceive) {
callType = Audio
} else {
callType = AudioSendOnly
}
} else {
const { listener } = config
let shouldSendReceive = false
if (listener != null) {
const { speakToInterpretationRoom } = listener
shouldSendReceive = speakToInterpretationRoom
}
if (shouldSendReceive) {
callType = Audio
} else {
callType = AudioRecvOnly
}
}

return callType
}

const getMediaStream = async (
constraints?: MediaTrackConstraints
): Promise<MediaStream> => {
Expand Down
31 changes: 31 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { logger } from './logger'

export const isSameDomain = (): boolean => {
// Check if the plugin is served from the same domain as Web App 3
let sameDomain = true
Expand All @@ -17,3 +19,32 @@ export const capitalizeFirstLetter = (value: string): string => {
}

export const isIOS = (): boolean => /iPad|iPhone|iPod/.test(navigator.userAgent)

export const pexHash = async (input: string): Promise<string | undefined> => {
try {
const hexRadix = 16
const decRadix = 10
const pad = 2

// Convert to byte
const encoder = new TextEncoder()
const data = encoder.encode(input)

// Use crypto of navigator
const hashBuffer = await crypto.subtle.digest('SHA-256', data)

// Convert to hex
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray
.map((b) => b.toString(hexRadix).padStart(pad, '0'))
.join('')

// Convert to dec
const decimal = BigInt('0x' + hashHex).toString(decRadix)

return decimal
} catch (error) {
logger.error(error)
return undefined
}
}