├── .babelrc ├── .gitignore ├── tsconfig.json ├── test └── test.js ├── rollup.config.js ├── demo ├── demo.ts ├── httpserver.js ├── certificate.crt ├── privateKey.key └── index.html ├── package.json ├── src ├── base.ts ├── rtc.ts ├── json-rpc.ts └── index.ts ├── dist └── verto.js ├── README.md └── stats.html /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.sublime-* 3 | src/*.js 4 | .size-snapshot.json 5 | dist 6 | tags 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./build/", 4 | "declaration": true, 5 | "sourceMap": true, 6 | "noImplicitAny": true, 7 | "module": "es2015", 8 | "target": "es6" 9 | }, 10 | "include": ["src"], 11 | "exclude": ["node_modules", "**/__tests__/*"] 12 | } 13 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { Verto } from '..'; 3 | 4 | describe('Distribution', function() { 5 | describe('import { Verto }', function() { 6 | it('should successfuly import Verto', function() { 7 | assert.notEqual( Verto, undefined ); 8 | }); 9 | it('should be a class', function() { 10 | assert.equal(typeof Verto, 'function'); 11 | }); 12 | it('should be able to produce a class instance', function() { 13 | let verto = new Verto({debug: true}); 14 | assert.equal(verto.debug, true) 15 | }); 16 | }); 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typeScript from 'rollup-plugin-typescript2'; 2 | import visualizer from 'rollup-plugin-visualizer'; 3 | import {sizeSnapshot} from "rollup-plugin-size-snapshot"; 4 | import {terser} from 'rollup-plugin-terser'; 5 | 6 | export default [ 7 | { 8 | input: 'src/index.ts', 9 | output: { 10 | file: 'dist/verto.js', 11 | format: 'iife', 12 | name: 'Verto', 13 | }, 14 | plugins: [ 15 | typeScript({tsconfig: "tsconfig.json"}), 16 | sizeSnapshot(), 17 | terser(), 18 | visualizer() 19 | ] 20 | }, 21 | { 22 | input: 'src/index.ts', 23 | output: { 24 | file: 'dist/index.js', 25 | format: 'umd', 26 | name: 'Verto', 27 | }, 28 | plugins: [ 29 | typeScript({tsconfig: "tsconfig.json"}), 30 | sizeSnapshot(), 31 | terser(), 32 | visualizer() 33 | ] 34 | }, 35 | ]; 36 | -------------------------------------------------------------------------------- /demo/demo.ts: -------------------------------------------------------------------------------- 1 | import { Verto } from '.' 2 | 3 | let verto = new Verto({transportConfig:{ 4 | login: '****', 5 | passwd: '****', 6 | socketUrl: 'wss://****:8082' 7 | },rtcConfig:{}}) 8 | 9 | 10 | async function main(){ 11 | try { 12 | let data = await verto.login() 13 | 14 | let stream = await navigator.mediaDevices.getUserMedia({audio:true}) 15 | 16 | verto.subscribeEvent('invite', call => { 17 | 18 | call.subscribeEvent('track', (track:any) => { 19 | if(track.kind!='audio') return 20 | let stream = new MediaStream() 21 | stream.addTrack(track) 22 | let el:HTMLMediaElement = document.getElementById('video') 23 | el.srcObject = stream 24 | console.log('track received', track) 25 | }) 26 | 27 | call.answer(stream.getTracks()) 28 | 29 | }) 30 | // let call = verto.call(stream.getTracks(),'9664',{}) 31 | } catch (error) { 32 | console.log(error) 33 | } 34 | } 35 | 36 | main() -------------------------------------------------------------------------------- /demo/httpserver.js: -------------------------------------------------------------------------------- 1 | const http = require('https') 2 | const fs = require('fs') 3 | const url = require('url') 4 | const path = require('path') 5 | const mime = require('mime') 6 | 7 | var options = { 8 | key: fs.readFileSync('privateKey.key'), 9 | cert: fs.readFileSync('certificate.crt') 10 | } 11 | 12 | var server = http.createServer(options, function(request, response) { 13 | var pathname = url.parse(request.url).pathname 14 | var filename = path.join(process.cwd(), '', pathname) 15 | if (!path.extname(filename)) { 16 | filename = filename + '/index.html'; 17 | } 18 | if (fs.existsSync(filename)) { 19 | response.writeHead(200, {'Content-Type': mime.getType(filename)}) 20 | fs.createReadStream(filename, { 21 | 'flags': 'r', 22 | 'encoding': 'binary', 23 | 'mode': 0o666, 24 | 'bufferSize': 4 * 1024 25 | }).addListener( "data", function(chunk) { 26 | response.write(chunk, 'binary') 27 | }).addListener( "close",function() { 28 | response.end() 29 | }) 30 | } else { 31 | response.writeHead(404, {'Content-Type': 'text/html'}) 32 | response.end() 33 | } 34 | }) 35 | 36 | server.listen(443) 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vertojs", 3 | "version": "0.0.5", 4 | "description": "Verto FreeSWITCH interface", 5 | "type": "module", 6 | "main": "src/index.ts", 7 | "author": "Roman Yerin ", 8 | "license": "BSD-3-Clause", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/roman-yerin/vertojs.git" 12 | }, 13 | "scripts": { 14 | "build": "npx rollup -c", 15 | "test": "npx mocha --require @babel/register" 16 | }, 17 | "keywords": [ 18 | "verto", 19 | "freeswitch" 20 | ], 21 | "bugs": { 22 | "url": "https://github.com/roman-yerin/vertojs/issues" 23 | }, 24 | "homepage": "https://github.com/roman-yerin/vertojs#readme", 25 | "devDependencies": { 26 | "@babel/cli": "^7.16.7", 27 | "@babel/core": "^7.16.7", 28 | "@babel/preset-env": "^7.16.7", 29 | "@babel/register": "^7.16.7", 30 | "@types/ws": "^6.0.3", 31 | "https": "^1.0.0", 32 | "mime": "^2.4.4", 33 | "mocha": "^9.1.3", 34 | "rollup": "^2.62.0", 35 | "rollup-plugin-size-snapshot": "^0.12.0", 36 | "rollup-plugin-terser": "^7.0.2", 37 | "rollup-plugin-typescript2": "^0.31.1", 38 | "rollup-plugin-visualizer": "^5.5.2", 39 | "source-map-loader": "^0.2.4", 40 | "ts-loader": "^6.2.0", 41 | "typescript": "^3.6.3", 42 | "ws": "^7.1.2" 43 | }, 44 | "dependencies": {} 45 | } 46 | -------------------------------------------------------------------------------- /demo/certificate.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDsTCCApmgAwIBAgIJAOKDg/pYqUvxMA0GCSqGSIb3DQEBCwUAMG8xCzAJBgNV 3 | BAYTAlJVMQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQHDARUZXN0MQ0wCwYDVQQKDARU 4 | ZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3QxHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhh 5 | bXBsZS5jb20wHhcNMTcxMTAyMTYzODQyWhcNMTgxMTAyMTYzODQyWjBvMQswCQYD 6 | VQQGEwJSVTENMAsGA1UECAwEVGVzdDENMAsGA1UEBwwEVGVzdDENMAsGA1UECgwE 7 | VGVzdDESMBAGA1UEAwwJbG9jYWxob3N0MR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4 8 | YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4cOLkwqe 9 | Bv+5weOEoLpwljhFN2bLlkCUuMmgE0G+KcEw/em3i3oyG5ij7ahZfy5ujxwZUu2w 10 | OTnn8bdFvR5vBz0kcUYJ2Sl8cVqzdzq5X34B5uknKyjCN5r3waffvbNCQW9YbQgA 11 | IqlafTWJ7/qI6uAmNI8urKtcJdCqz4jSjcQ7HShNftgk34TYyo1QoVb4apWACGBR 12 | IIo80hNBlLhZS9JULhIDvcViCAYtad6XkiooBJUHHwde9OLnMtipGniDFheTxTcx 13 | hj3DpbI/7PggJGGoVw6wyb0sb+RwF6+AydsbmcD2NIGapM5qkQr/e7dLfL4ReT0U 14 | QgfNcHUHaV39owIDAQABo1AwTjAdBgNVHQ4EFgQUk1KSa7nx4Io4181JPr/VKDr/ 15 | OOAwHwYDVR0jBBgwFoAUk1KSa7nx4Io4181JPr/VKDr/OOAwDAYDVR0TBAUwAwEB 16 | /zANBgkqhkiG9w0BAQsFAAOCAQEATZpgT03tZM54ZCDN5E77nQcPw3s/RpC/D5aP 17 | MF1J/tAy1HpHw4UqOG3pumfH8Y0exXR+C2PdKL/ztG0HKIzU2xevPQ4y+DtQBChA 18 | 1FmnUz/eWPxqkmiWG7CLeMQN5NPSYJFzB71JZMCgdnm12PThBba3UNyhcaEj8LL+ 19 | Eviy0KtgbvRb9+nBtHl8lBZ41W5xJBPDZkq50XoSSxv9OLU4OHw6oaXWFoE4UiP3 20 | JcesNwUr9GghARE9o3ujoGwD7MaePt3WtOvVF2RUntTTwhfXxq4umbhd4Wsx001c 21 | sJ86d1kBuU0YrJDaKO67pK3EFe6xnxuyf6RerZQNuzEfSYI7WA== 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /demo/privateKey.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDhw4uTCp4G/7nB 3 | 44SgunCWOEU3ZsuWQJS4yaATQb4pwTD96beLejIbmKPtqFl/Lm6PHBlS7bA5Oefx 4 | t0W9Hm8HPSRxRgnZKXxxWrN3OrlffgHm6ScrKMI3mvfBp9+9s0JBb1htCAAiqVp9 5 | NYnv+ojq4CY0jy6sq1wl0KrPiNKNxDsdKE1+2CTfhNjKjVChVvhqlYAIYFEgijzS 6 | E0GUuFlL0lQuEgO9xWIIBi1p3peSKigElQcfB1704ucy2KkaeIMWF5PFNzGGPcOl 7 | sj/s+CAkYahXDrDJvSxv5HAXr4DJ2xuZwPY0gZqkzmqRCv97t0t8vhF5PRRCB81w 8 | dQdpXf2jAgMBAAECggEAOrJLhJRbF7i/BADyR5ORmAzTOh84yTzLLewF+Vu7ZEG9 9 | ShbMyK+hfXlHDZAajK12wzBsCRqRGZ/LhRXARPY3qask4WpzPwnBN/t784DKF2C8 10 | f9uYemkjba+VwLyzuC92B2s6k0ZkOp7LTwhvb3w0wtQ6OqoLWYtH7vD8p+6Lx52D 11 | LRwaWN/FPbJLcTVQK13MI4FqKI/n4Q1ZA8rnSTinOD+Gdur9ndLFs3c/IISECi1L 12 | IhsTNIzdv8pN/AVKcc1xYtSIcXotf1RiVaNTAmpMmw9mX4a/cJYuKV4PtXEEzciL 13 | WVdqnzmcpIXJf59HEs/gwCIHp1fOxK2u72TeVD5YeQKBgQD3hTsi5cvQktmQwZ4r 14 | P1Mv4C0g7gdHcwbWPRnnoYZiU7yhCE6/T2IlZy2r4IflrepkobsG6kMtEpzJSFGq 15 | nf5hnhCXoT5vHhRIFHLtcA71NWHKewP27OLVFW3coFmPJ/STRxdoUHOu4KqfjleR 16 | iqD/YI7PWbdvFk438K3Yn3i8XwKBgQDpf4H18+3AxLDLEdzAI3gIw+XI63iJiSLL 17 | CFzAVs68vHT5Or6092dn3WUI8IQqTSi9wtLzICMKA6/v0Qbf2aURSJTix3zLN/Ne 18 | uGQTvDBSn17MjXZzf6XCf7SJC/XzF78PwRCXgwYJAGVjl32Og2FGgX9EJVRdF3cf 19 | PcOUwdzFPQKBgQDmkuvltF9KqqGVoWewLctWW+RuOo35VwPVaxHInsVKr2qWL+D7 20 | gf2Rji4TYJP3ty0UFTzeUjfFswLu1jmNUKR5Vv9p4MECTejixHnTCYJFljbEohet 21 | XDpp+Q5gaddD7hp9X8pEWD2LeKo4/CZC4/raKp6eNZsVFphCsCeiFdwozQKBgQCn 22 | UXldN4hdJBTTz17yR/hRv56/VQyw8ZX/C2T7ZrkKQblIhrH1l3t/0AQAXek3LsdN 23 | A3iKQ7MYEABYxt44Ngu00N/viaeBL/yzGUqNYcL20cDqr0v8A7JSJ5TEx89cfN2q 24 | elxbVcZTWPdOYFXWc4qXLWB3ApDrjE8OEgI+bJS4uQKBgCbq9DAb58f2Ub4LXbnv 25 | dsHPgfftSkqcWEy3tGjXDjSBHXPTWe/O6nSeIUO6XP7uWmLV6eVKWTkYdmic7ITu 26 | gxhnUyy1Y0w5yKe6sJ4nmB5hX1IFWXp4aySsWrNVREtUiipsLC3oBVkWr3iQuZ/B 27 | vMpbOWFaI92rXV19nIUy3tMs 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /src/base.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Verto FreeSWITCH interface 4 | 5 | Copyright (c) 2019 Roman Yerin 6 | 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | * Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | * Neither the name of the nor the 17 | names of its contributors may be used to endorse or promote products 18 | derived from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 24 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | */ 32 | 33 | const generateGUID = (typeof window !== 'undefined' && typeof(window.crypto) !== 'undefined' && typeof(window.crypto.getRandomValues) !== 'undefined') ? 34 | function(): string { 35 | let buf = new Uint16Array(8) 36 | window.crypto.getRandomValues(buf) 37 | let S4 = function(num:number) { 38 | let ret = num.toString(16) 39 | while (ret.length < 4) { 40 | ret = "0" + ret 41 | } 42 | return ret 43 | } 44 | return (S4(buf[0]) + S4(buf[1]) + "-" + S4(buf[2]) + "-" + S4(buf[3]) + "-" + S4(buf[4]) + "-" + S4(buf[5]) + S4(buf[6]) + S4(buf[7])) 45 | } 46 | 47 | : 48 | 49 | function(): string { 50 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 51 | let r = Math.random() * 16 | 0, 52 | v = c == 'x' ? r : (r & 0x3 | 0x8) 53 | return v.toString(16) 54 | }) 55 | } 56 | 57 | 58 | interface VertoEventHandler { 59 | code: {(data: any):void}, 60 | id: string 61 | } 62 | 63 | class VertoBase { 64 | 65 | private event_handlers: {[name: string]: Array} = {} 66 | public debug: boolean 67 | 68 | constructor(debug?: boolean){ 69 | this.debug = debug 70 | } 71 | 72 | subscribeEvent(name: string, handler: {(data:any):void}):string{ 73 | let id = generateGUID() 74 | if(!this.event_handlers[name]) this.event_handlers[name] = [] 75 | this.event_handlers[name].push({id, code: handler}) 76 | return id 77 | } 78 | 79 | unsubscribeEvent(name: string, handlerID?: string){ 80 | if(handlerID) { 81 | this.event_handlers[name] = this.event_handlers[name].map((v, i, a) => { if(v.id == handlerID) return; else return v; }) 82 | } else { 83 | this.event_handlers[name] = [] 84 | } 85 | } 86 | 87 | dispatchEvent(name: string, data?: any){ 88 | if(this.debug) console.log('Dispatch', name, data) 89 | if(this.event_handlers[name]) 90 | for(let h of this.event_handlers[name]){ 91 | h.code(data) 92 | } 93 | } 94 | 95 | } 96 | 97 | export { VertoBase, generateGUID } 98 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 31 | 32 | 33 | 34 |
35 |

Verto JS demo

36 | 37 | 38 | 39 | 40 |
41 |
42 | 43 | 44 | 45 |
46 | 127 | 128 | -------------------------------------------------------------------------------- /src/rtc.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Verto FreeSWITCH interface 4 | 5 | Copyright (c) 2019 Roman Yerin 6 | 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | * Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | * Neither the name of the nor the 17 | names of its contributors may be used to endorse or promote products 18 | derived from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 24 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | */ 32 | 33 | import { VertoBase } from './base' 34 | 35 | enum CallDirection { 36 | Incoming, 37 | Outgoing 38 | } 39 | 40 | enum CallState { 41 | None, 42 | Schedulled, 43 | MessageSent, 44 | } 45 | 46 | /* 47 | 48 | Verto RTC is an interface to WebRTC 49 | 50 | */ 51 | 52 | class VertoRtc extends VertoBase{ 53 | private pc : RTCPeerConnection 54 | private state : CallState = CallState.None 55 | private presdp : string 56 | private direction : CallDirection = CallDirection.Incoming 57 | private ice_timer : ReturnType 58 | private ice_timeout : number 59 | 60 | constructor(conf: RTCConfiguration, direction?: CallDirection, ice_timeout?: number, debug?: boolean) { 61 | super(debug) 62 | this.ice_timeout = (ice_timeout?ice_timeout:3000) 63 | conf.iceCandidatePoolSize = ('iceCandidatePoolSize' in conf? conf.iceCandidatePoolSize: 1) 64 | this.pc = new RTCPeerConnection(conf) 65 | this.pc.ontrack = this.onTrack.bind(this) 66 | this.pc.onnegotiationneeded = this.onNegotiation.bind(this) 67 | this.pc.onicecandidate = this.onCandidate.bind(this) 68 | this.pc.onicegatheringstatechange = this.onIceGatheringStateChange.bind(this) 69 | if(typeof direction!== 'undefined') this.direction = direction 70 | } 71 | 72 | private onTrack(event: RTCTrackEvent) { 73 | this.dispatchEvent('track',event.track) 74 | } 75 | 76 | private onCandidate(event: RTCPeerConnectionIceEvent) { 77 | } 78 | 79 | private onIceGatheringStateChange(event: Event){ 80 | if(this.pc.iceGatheringState == 'complete') { 81 | if(this.ice_timer) clearTimeout(this.ice_timer) 82 | if(this.state == CallState.MessageSent) return // Offer or answer is already sent 83 | if(this.direction) 84 | this.dispatchEvent('send-offer',this.pc.localDescription) 85 | else 86 | this.dispatchEvent('send-answer',this.pc.localDescription) 87 | } 88 | } 89 | 90 | private iceTimerTriggered() { 91 | if(this.debug) console.log(this.pc) 92 | if(this.state != CallState.Schedulled) return // The call is not in schedulled state, do nothing 93 | this.state = CallState.MessageSent 94 | if(this.direction) 95 | this.dispatchEvent('send-offer',this.pc.localDescription) 96 | else 97 | this.dispatchEvent('send-answer',this.pc.localDescription) 98 | } 99 | 100 | private onNegotiation() { 101 | this.pc 102 | .createOffer() 103 | .then(offer => { 104 | this.ice_timer = setTimeout(this.iceTimerTriggered.bind(this), this.ice_timeout) 105 | return this.pc.setLocalDescription(offer) 106 | }) 107 | .then(() => { 108 | // Schedulle offer to remote host 109 | this.state = CallState.Schedulled 110 | }) 111 | .catch(error => {}) 112 | } 113 | 114 | getStats() { 115 | return this.pc ? this.pc.getStats() : Promise.reject() 116 | } 117 | 118 | onMedia(sdp: string) { 119 | this.pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp})) 120 | .then(() => { 121 | if(this.debug) console.log('got remote media') 122 | }) 123 | .catch(error => { 124 | if(this.debug) console.log('remote media error', error) 125 | }) 126 | } 127 | 128 | preSdp(sdp: string){ 129 | this.presdp = sdp 130 | } 131 | 132 | addTrack(track: MediaStreamTrack) { 133 | this.pc.addTrack(track) 134 | } 135 | 136 | answer(tracks: Array) { 137 | for(let t of tracks) this.pc.addTrack(t) 138 | this.ice_timer = setTimeout(this.iceTimerTriggered.bind(this), this.ice_timeout) 139 | this.pc.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: this.presdp})) 140 | .then(() => { 141 | this.pc.createAnswer() 142 | .then(description => { 143 | this.pc.setLocalDescription(description) 144 | this.state = CallState.Schedulled 145 | }) 146 | }) 147 | } 148 | 149 | } 150 | 151 | export { VertoRtc, CallDirection } 152 | -------------------------------------------------------------------------------- /src/json-rpc.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Verto FreeSWITCH interface 4 | 5 | Copyright (c) 2019 Roman Yerin 6 | 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | * Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | * Neither the name of the nor the 17 | names of its contributors may be used to endorse or promote products 18 | derived from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 24 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | */ 32 | 33 | /* 34 | 35 | Interface to JSON RPC Client constructor 36 | 37 | */ 38 | 39 | interface JsonRpcClientParams { 40 | socketUrl : string 41 | ajaxUrl? : string 42 | onmessage? : {(event:Object):void} 43 | login : string 44 | passwd : string 45 | sessid? : string 46 | loginParams? : Object 47 | userVariables? : Object 48 | } 49 | 50 | /* 51 | 52 | Interface to JSON RPC requests 53 | 54 | */ 55 | 56 | interface JsonRpcRequest { 57 | id : number 58 | jsonrpc : string 59 | method : string 60 | params : Object 61 | } 62 | 63 | /* 64 | 65 | Interface to JSON RPC replies 66 | 67 | */ 68 | 69 | interface JsonRpcReply { 70 | id : number 71 | jsonrpc : string 72 | method? : string 73 | params? : JsonRpcParams 74 | result? : any 75 | error? : {code?:number} 76 | } 77 | 78 | /* 79 | 80 | Interface to JSON RPC calls 81 | 82 | */ 83 | 84 | interface JsonRpcCall { 85 | request: JsonRpcRequest 86 | success_cb?: {(data: Object): void} 87 | error_cb?: {(data: Object): void} 88 | } 89 | 90 | interface JsonRpcParams { 91 | callID : string 92 | sdp? : string 93 | caller_id_number : string 94 | caller_id_name : string 95 | callee_id_number : string 96 | callee_id_name : string 97 | 98 | } 99 | 100 | /* 101 | 102 | JSON RPC Client 103 | 104 | */ 105 | 106 | class JsonRpcClient { 107 | 108 | options: JsonRpcClientParams 109 | private request_id: number = 1 110 | private socket_: WebSocket 111 | private queue: Array = [] 112 | private callbacks: Array = [] 113 | private eventHandler: {(method:string, params:JsonRpcParams):void} 114 | private reconnectHandler: {():void} 115 | private debug: boolean = false 116 | 117 | private initSocket_(){ 118 | this.socket_ = new WebSocket(this.options.socketUrl) 119 | this.socket_.onmessage = this.onMessage.bind(this) 120 | // TODO: Implement auto reconnect attepts 121 | this.socket_.onclose = () => { 122 | setTimeout(() => { 123 | if(this.debug) console.log("WSS reconnect") 124 | this.initSocket_() 125 | }, 1000) 126 | } 127 | this.socket_.onopen = () => { 128 | let req: string = "" 129 | if(this.reconnectHandler) this.reconnectHandler() 130 | 131 | while(req = this.queue.pop()){ 132 | this.socket.send(req) 133 | } 134 | } 135 | } 136 | 137 | private closeSocket_(){ 138 | this.socket_.onclose = () => { 139 | console.log("WebSocket is closed now."); 140 | } 141 | this.socket_.close() 142 | } 143 | 144 | private get socket(): WebSocket { 145 | 146 | if(this.socket_) { 147 | return this.socket_ 148 | } 149 | this.initSocket_() 150 | return this.socket_ 151 | 152 | } 153 | 154 | constructor(options: JsonRpcClientParams, debug?: boolean) { 155 | this.debug = debug 156 | this.options = Object.assign({ 157 | }, options) 158 | } 159 | 160 | private onMessage(msg:MessageEvent){ 161 | // Check if this is JSON RPC 162 | 163 | let response: JsonRpcReply 164 | try{ 165 | response = JSON.parse(msg.data) 166 | } catch(error) { 167 | // Got something else, just ignore in case 168 | } 169 | 170 | if(this.debug) console.log(response) 171 | 172 | if(typeof response === 'object' 173 | && 'jsonrpc' in response 174 | && response['jsonrpc'] === '2.0' 175 | ) { 176 | if ('method' in response){ 177 | this.eventHandler(response.method, response.params) 178 | } 179 | else if ('result' in response && this.callbacks[response.id]) { 180 | // We've just got response 181 | let success_cb = this.callbacks[response.id].success_cb 182 | delete this.callbacks[response['id']] 183 | success_cb(Object.assign({}, response.result)) 184 | } else if('error' in response && this.callbacks[response.id]) { 185 | let error_cb = this.callbacks[response.id].error_cb 186 | let failed_request = Object.assign({}, this.callbacks[response.id]) 187 | 188 | delete this.callbacks[response['id']] 189 | 190 | if(response.error.code == -32000 && this.options.login && this.options.passwd) { 191 | // Auth is needed 192 | this.call('login', { 193 | login: this.options.login, 194 | passwd: this.options.passwd, 195 | loginParams: this.options.loginParams, 196 | userVariables: this.options.userVariables 197 | }, 198 | (data) => { 199 | // Re-send failed request 200 | this.socketCall_(failed_request) 201 | }, 202 | (data) => { 203 | // Auth failed 204 | error_cb(Object.assign({}, response.result)) 205 | }) 206 | } else { 207 | error_cb(Object.assign({}, response.result)) 208 | } 209 | } 210 | } 211 | } 212 | 213 | private socketCall_({request, success_cb, error_cb}: JsonRpcCall) { 214 | request.id = this.request_id++ 215 | 216 | let rawData = JSON.stringify(request) 217 | 218 | this.callbacks[request.id] = { request, success_cb, error_cb } 219 | 220 | if(this.socket.readyState < 1) { 221 | // Socket is not ready, queue message 222 | this.queue.push(rawData) 223 | if(this.debug) console.log('Queued', rawData) 224 | } else { 225 | this.socket.send(rawData) 226 | if(this.debug) console.log('Sent', rawData) 227 | } 228 | } 229 | 230 | public setEventHandler(handler: {(method: string, params: JsonRpcParams):void}){ 231 | this.eventHandler = handler 232 | } 233 | 234 | public setReconnectHandler(handler: {():void}){ 235 | this.reconnectHandler = handler 236 | } 237 | 238 | public call(method: string, params?: Object, success_cb?: {(data: Object): void}, error_cb?: {(data: Object): void}) { 239 | // Construct the JSON-RPC 2.0 request. 240 | let call_params = Object.assign({}, params) 241 | 242 | let request = { 243 | jsonrpc : '2.0', 244 | method : method, 245 | params : call_params, 246 | id : 0 247 | } 248 | 249 | if(this.socket) { 250 | this.socketCall_({request, success_cb, error_cb}) 251 | } 252 | } 253 | 254 | public close() { 255 | this.queue = [] 256 | this.callbacks = [] 257 | this.eventHandler = null 258 | this.reconnectHandler = null 259 | this.closeSocket_() 260 | } 261 | } 262 | 263 | export { JsonRpcClient, JsonRpcClientParams } 264 | 265 | -------------------------------------------------------------------------------- /dist/verto.js: -------------------------------------------------------------------------------- 1 | var Verto=function(t){"use strict";class e{constructor(t,e){this.request_id=1,this.queue=[],this.callbacks=[],this.debug=!1,this.debug=e,this.options=Object.assign({},t)}initSocket_(){this.socket_=new WebSocket(this.options.socketUrl),this.socket_.onmessage=this.onMessage.bind(this),this.socket_.onclose=()=>{setTimeout((()=>{this.debug&&console.log("WSS reconnect"),this.initSocket_()}),1e3)},this.socket_.onopen=()=>{let t="";for(this.reconnectHandler&&this.reconnectHandler();t=this.queue.pop();)this.socket.send(t)}}closeSocket_(){this.socket_.onclose=()=>{console.log("WebSocket is closed now.")},this.socket_.close()}get socket(){return this.socket_||this.initSocket_(),this.socket_}onMessage(t){let e;try{e=JSON.parse(t.data)}catch(t){}if(this.debug&&console.log(e),"object"==typeof e&&"jsonrpc"in e&&"2.0"===e.jsonrpc)if("method"in e)this.eventHandler(e.method,e.params);else if("result"in e&&this.callbacks[e.id]){let t=this.callbacks[e.id].success_cb;delete this.callbacks[e.id],t(Object.assign({},e.result))}else if("error"in e&&this.callbacks[e.id]){let t=this.callbacks[e.id].error_cb,s=Object.assign({},this.callbacks[e.id]);delete this.callbacks[e.id],-32e3==e.error.code&&this.options.login&&this.options.passwd?this.call("login",{login:this.options.login,passwd:this.options.passwd,loginParams:this.options.loginParams,userVariables:this.options.userVariables},(t=>{this.socketCall_(s)}),(s=>{t(Object.assign({},e.result))})):t(Object.assign({},e.result))}}socketCall_({request:t,success_cb:e,error_cb:s}){t.id=this.request_id++;let i=JSON.stringify(t);this.callbacks[t.id]={request:t,success_cb:e,error_cb:s},this.socket.readyState<1?(this.queue.push(i),this.debug&&console.log("Queued",i)):(this.socket.send(i),this.debug&&console.log("Sent",i))}setEventHandler(t){this.eventHandler=t}setReconnectHandler(t){this.reconnectHandler=t}call(t,e,s,i){let n={jsonrpc:"2.0",method:t,params:Object.assign({},e),id:0};this.socket&&this.socketCall_({request:n,success_cb:s,error_cb:i})}close(){this.queue=[],this.callbacks=[],this.eventHandler=null,this.reconnectHandler=null,this.closeSocket_()}}const s="undefined"!=typeof window&&void 0!==window.crypto&&void 0!==window.crypto.getRandomValues?function(){let t=new Uint16Array(8);window.crypto.getRandomValues(t);let e=function(t){let e=t.toString(16);for(;e.length<4;)e="0"+e;return e};return e(t[0])+e(t[1])+"-"+e(t[2])+"-"+e(t[3])+"-"+e(t[4])+"-"+e(t[5])+e(t[6])+e(t[7])}:function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(function(t){let e=16*Math.random()|0;return("x"==t?e:3&e|8).toString(16)}))};class i{constructor(t){this.event_handlers={},this.debug=t}subscribeEvent(t,e){let i=s();return this.event_handlers[t]||(this.event_handlers[t]=[]),this.event_handlers[t].push({id:i,code:e}),i}unsubscribeEvent(t,e){this.event_handlers[t]=e?this.event_handlers[t].map(((t,s,i)=>t.id==e?void 0:t)):[]}dispatchEvent(t,e){if(this.debug&&console.log("Dispatch",t,e),this.event_handlers[t])for(let s of this.event_handlers[t])s.code(e)}}var n,o;t.CallDirection=void 0,(n=t.CallDirection||(t.CallDirection={}))[n.Incoming=0]="Incoming",n[n.Outgoing=1]="Outgoing",function(t){t[t.None=0]="None",t[t.Schedulled=1]="Schedulled",t[t.MessageSent=2]="MessageSent"}(o||(o={}));class c extends i{constructor(e,s,i,n){super(n),this.state=o.None,this.direction=t.CallDirection.Incoming,this.ice_timeout=i||3e3,e.iceCandidatePoolSize="iceCandidatePoolSize"in e?e.iceCandidatePoolSize:1,this.pc=new RTCPeerConnection(e),this.pc.ontrack=this.onTrack.bind(this),this.pc.onnegotiationneeded=this.onNegotiation.bind(this),this.pc.onicecandidate=this.onCandidate.bind(this),this.pc.onicegatheringstatechange=this.onIceGatheringStateChange.bind(this),void 0!==s&&(this.direction=s)}onTrack(t){this.dispatchEvent("track",t.track)}onCandidate(t){}onIceGatheringStateChange(t){if("complete"==this.pc.iceGatheringState){if(this.ice_timer&&clearTimeout(this.ice_timer),this.state==o.MessageSent)return;this.direction?this.dispatchEvent("send-offer",this.pc.localDescription):this.dispatchEvent("send-answer",this.pc.localDescription)}}iceTimerTriggered(){this.debug&&console.log(this.pc),this.state==o.Schedulled&&(this.state=o.MessageSent,this.direction?this.dispatchEvent("send-offer",this.pc.localDescription):this.dispatchEvent("send-answer",this.pc.localDescription))}onNegotiation(){this.pc.createOffer().then((t=>(this.ice_timer=setTimeout(this.iceTimerTriggered.bind(this),this.ice_timeout),this.pc.setLocalDescription(t)))).then((()=>{this.state=o.Schedulled})).catch((t=>{}))}getStats(){return this.pc?this.pc.getStats():Promise.reject()}onMedia(t){this.pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:t})).then((()=>{this.debug&&console.log("got remote media")})).catch((t=>{this.debug&&console.log("remote media error",t)}))}preSdp(t){this.presdp=t}addTrack(t){this.pc.addTrack(t)}answer(t){for(let e of t)this.pc.addTrack(e);this.ice_timer=setTimeout(this.iceTimerTriggered.bind(this),this.ice_timeout),this.pc.setRemoteDescription(new RTCSessionDescription({type:"offer",sdp:this.presdp})).then((()=>{this.pc.createAnswer().then((t=>{this.pc.setLocalDescription(t),this.state=o.Schedulled}))}))}}const a="verto.invite",h="verto.answer",l="verto.bye",r="verto.media",d="verto.modify",p="verto.info";class g extends i{constructor(e,i,n,o,l,r,d){super(d),this.id=o||s(),this.options=l||{},this.direction=n?t.CallDirection.Outgoing:t.CallDirection.Incoming,this.rtc=new c(e,this.direction,r,d),this.rpc=i,this.rtc.subscribeEvent("send-offer",(t=>{let e=Object.assign({},this.options);e=Object.assign(e,{destination_number:n,callID:this.id}),this.rpc.call(a,{dialogParams:e,sdp:t.sdp},(t=>{}),(t=>{}))})),this.rtc.subscribeEvent("send-answer",(t=>{this.rpc.call(h,{dialogParams:{destination_number:n,callID:this.id},sdp:t.sdp},(t=>{}),(t=>{}))})),this.rtc.subscribeEvent("track",(t=>{this.dispatchEvent("track",t)}))}getStats(){return this.rtc.getStats()}onAnswer(t){t&&this.rtc.onMedia(t),this.debug&&console.log("answer"),this.dispatchEvent("answer")}onMedia(t){this.rtc.onMedia(t),this.dispatchEvent("media")}addTrack(t){this.rtc.addTrack(t)}preSdp(t){this.rtc.preSdp(t)}answer(t){this.rtc.answer(t)}hangup(t={}){this.rpc.call(l,Object.assign({dialogParams:{callID:this.id}},t),(t=>{}),(t=>{})),this.clear()}clear(){this.dispatchEvent("bye",this)}dtmf(t){this.rpc.call(p,{dtmf:t.toString(),dialogParams:{callID:this.id}},(t=>{}),(t=>{}))}hold(t){this.rpc.call(d,{action:"hold",dialogParams:Object.assign(Object.assign({callID:this.id},this.options),t)},(t=>{"held"===t.holdState?(this.holdStatus=!0,this.dispatchEvent("hold",this)):(this.holdStatus=!1,this.dispatchEvent("unhold",this))}),(t=>{}))}unhold(t){this.rpc.call(d,{action:"unhold",dialogParams:Object.assign(Object.assign({callID:this.id},this.options),t)},(t=>{"held"===t.holdState?(this.holdStatus=!0,this.dispatchEvent("hold",this)):(this.holdStatus=!1,this.dispatchEvent("unhold",this))}),(t=>{}))}toggleHold(t){this.rpc.call(d,{action:"toggleHold",dialogParams:Object.assign(Object.assign({callID:this.id},this.options),t)},(t=>{"held"===t.holdState?(this.holdStatus=!0,this.dispatchEvent("hold",this)):(this.holdStatus=!1,this.dispatchEvent("unhold",this))}),(t=>{}))}}return t.Verto=class extends i{constructor(t){super(t.debug),this.calls={},this.logged_in=!1,this.options=t,this.rpc=new e(t.transportConfig,t.debug),this.rpc.setEventHandler(((t,e)=>{switch(t){case h:{let t=e.callID;this.calls[t].onAnswer(e.sdp);break}case r:{let t=e.callID;this.calls[t].onMedia(e.sdp);break}case a:{let t=new g(this.options.rtcConfig,this.rpc,"",e.callID,{caller_id_name:e.caller_id_name,caller_id_number:e.caller_id_number},this.options.ice_timeout,this.options.debug);t.preSdp(e.sdp),this.calls[e.callID]=t,this.dispatchEvent("invite",t);break}case l:this.calls[e.callID].clear(),delete this.calls[e.callID];break}})),this.rpc.setReconnectHandler((()=>{this.logged_in&&this.login()})),this.options.rtcConfig=Object.assign({},this.options.rtcConfig||{})}login(){return new Promise(((t,e)=>{this.rpc.call("login",{login:this.options.transportConfig.login,passwd:this.options.transportConfig.passwd},(e=>{this.sessid=e.sessid,this.logged_in=!0,t(e)}),(t=>{e(t)}))}))}call(t,e,i){let n=new g(this.options.rtcConfig,this.rpc,e,s(),i,this.options.ice_timeout,this.options.debug);for(let e of t)n.addTrack(e);return this.calls[n.id]=n,n}isLogged(){return this.logged_in}logout(){this.calls&&Object.keys(this.calls).forEach((t=>{this.calls[t].hangup()})),this.rpc.close(),this.rpc=null,this.sessid=null,this.logged_in=!1}},Object.defineProperty(t,"__esModule",{value:!0}),t}({}); 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Verto FreeSWITCH interface 4 | 5 | Copyright (c) 2019 Roman Yerin 6 | 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | * Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | * Neither the name of the nor the 17 | names of its contributors may be used to endorse or promote products 18 | derived from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 24 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | */ 32 | 33 | import { JsonRpcClient, JsonRpcClientParams } from './json-rpc' 34 | import { VertoRtc, CallDirection } from './rtc' 35 | import { VertoBase, generateGUID } from './base' 36 | 37 | interface VertoOptions { 38 | transportConfig : JsonRpcClientParams 39 | rtcConfig? : RTCConfiguration 40 | debug? : boolean 41 | ice_timeout? : number 42 | } 43 | 44 | interface VertoCallOptions { 45 | caller_id_number? : string 46 | caller_id_name? : string 47 | callee_id_number? : string 48 | callee_id_name? : string 49 | } 50 | 51 | const VertoMessage = { 52 | Invite: 'verto.invite', 53 | Answer: 'verto.answer', 54 | Bye: 'verto.bye', 55 | Media: 'verto.media', 56 | Attach: 'verto.attach', 57 | Modify: 'verto.modify', 58 | Subscribe: 'verto.subscribe', 59 | Unsubscribe: 'verto.unsubscribe', 60 | Info: 'verto.info', 61 | Display: 'verto.display', 62 | ClientReady: 'verto.clientReady', 63 | Broadcast: 'verto.broadcast' 64 | } 65 | 66 | class VertoCall extends VertoBase{ 67 | 68 | private rtc: VertoRtc 69 | private rpc: JsonRpcClient 70 | public id: string 71 | public options: VertoCallOptions 72 | public direction: CallDirection 73 | public holdStatus: boolean 74 | 75 | constructor(conf: RTCConfiguration, rpc: JsonRpcClient, dest?: string, id?:string, options?: VertoCallOptions, ice_timeout?: number, debug?: boolean){ 76 | super(debug) 77 | this.id = id || generateGUID() 78 | this.options = options || {} 79 | this.direction = dest?CallDirection.Outgoing:CallDirection.Incoming 80 | this.rtc = new VertoRtc(conf, this.direction, ice_timeout, debug) 81 | this.rpc = rpc 82 | 83 | this.rtc.subscribeEvent('send-offer', sessionDescription => { 84 | let dialogParams = Object.assign({}, this.options) 85 | dialogParams = Object.assign(dialogParams, {destination_number: dest, callID: this.id}) 86 | 87 | this.rpc.call(VertoMessage.Invite, { dialogParams, sdp: sessionDescription.sdp}, data => {}, data => {}) 88 | }) 89 | 90 | this.rtc.subscribeEvent('send-answer', sessionDescription => { 91 | this.rpc.call(VertoMessage.Answer, { dialogParams: {destination_number: dest, callID: this.id}, sdp: sessionDescription.sdp}, data => {}, data => {}) 92 | }) 93 | 94 | this.rtc.subscribeEvent('track', track => { 95 | this.dispatchEvent('track', track) 96 | }) 97 | 98 | } 99 | 100 | getStats() { 101 | return this.rtc.getStats() 102 | } 103 | 104 | onAnswer(sdp: string) { 105 | if (sdp) { 106 | this.rtc.onMedia(sdp) 107 | } 108 | if(this.debug) console.log('answer') 109 | this.dispatchEvent('answer') 110 | } 111 | 112 | onMedia(sdp: string) { 113 | this.rtc.onMedia(sdp) 114 | this.dispatchEvent('media') 115 | } 116 | 117 | addTrack(track: MediaStreamTrack){ 118 | this.rtc.addTrack(track) 119 | } 120 | 121 | preSdp(sdp: string) { 122 | this.rtc.preSdp(sdp) 123 | } 124 | 125 | answer(tracks: Array) { 126 | this.rtc.answer(tracks) 127 | } 128 | 129 | hangup(params = {}) { 130 | this.rpc.call(VertoMessage.Bye, { dialogParams: {callID: this.id}, ...params}, data => {}, data => {}) 131 | this.clear() 132 | } 133 | 134 | clear() { 135 | this.dispatchEvent('bye', this) 136 | } 137 | 138 | dtmf(input: string) { 139 | this.rpc.call(VertoMessage.Info, { dtmf: input.toString(), dialogParams: {callID: this.id}}, data => {}, data => {}) 140 | } 141 | 142 | hold(params?: object) { 143 | this.rpc.call(VertoMessage.Modify, {action: "hold", dialogParams: {callID: this.id, ...this.options, ...params}},(data: {holdState:string}) => { 144 | if (data.holdState === 'held') { 145 | this.holdStatus = true 146 | this.dispatchEvent('hold', this) 147 | } else { 148 | this.holdStatus = false 149 | this.dispatchEvent('unhold', this) 150 | } 151 | }, data => {}) 152 | } 153 | 154 | unhold(params?: object) { 155 | this.rpc.call(VertoMessage.Modify, {action: "unhold", dialogParams: {callID: this.id, ...this.options, ...params}},(data: {holdState:string}) => { 156 | if (data.holdState === 'held') { 157 | this.holdStatus = true 158 | this.dispatchEvent('hold', this) 159 | } else { 160 | this.holdStatus = false 161 | this.dispatchEvent('unhold', this) 162 | } 163 | }, data => {}) 164 | } 165 | 166 | toggleHold(params?: object) { 167 | this.rpc.call(VertoMessage.Modify, {action: "toggleHold", dialogParams: {callID: this.id, ...this.options, ...params}},(data: {holdState:string}) => { 168 | if (data.holdState === 'held') { 169 | this.holdStatus = true 170 | this.dispatchEvent('hold', this) 171 | } else { 172 | this.holdStatus = false 173 | this.dispatchEvent('unhold', this) 174 | } 175 | }, data => {}) 176 | } 177 | 178 | } 179 | 180 | class Verto extends VertoBase{ 181 | 182 | private calls : {[key:string]: VertoCall} = {} 183 | private rpc : JsonRpcClient 184 | private options : VertoOptions 185 | private sessid : string 186 | private logged_in : boolean = false 187 | 188 | constructor(options: VertoOptions) { 189 | // 190 | super(options.debug) 191 | this.options = options 192 | this.rpc = new JsonRpcClient(options.transportConfig, options.debug) 193 | this.rpc.setEventHandler((method, params) => { 194 | switch(method) { 195 | case VertoMessage.Answer: { 196 | let callID: string = params.callID 197 | this.calls[callID].onAnswer(params.sdp) 198 | break 199 | } 200 | case VertoMessage.Media: { 201 | let callID: string = params.callID 202 | this.calls[callID].onMedia(params.sdp) 203 | break 204 | } 205 | case VertoMessage.Invite: { 206 | let call = new VertoCall(this.options.rtcConfig,this.rpc,'',params.callID, {caller_id_name: params.caller_id_name, caller_id_number: params.caller_id_number}, this.options.ice_timeout, this.options.debug) 207 | call.preSdp(params.sdp) 208 | this.calls[params.callID] = call 209 | this.dispatchEvent('invite',call) 210 | break 211 | } 212 | case VertoMessage.Bye: { 213 | let call = this.calls[params.callID] 214 | call.clear() 215 | delete this.calls[params.callID] 216 | break 217 | } 218 | } 219 | }) 220 | this.rpc.setReconnectHandler(() => { 221 | if(this.logged_in) this.login() 222 | }) 223 | this.options.rtcConfig = Object.assign( 224 | {}, this.options.rtcConfig || {}) 225 | 226 | } 227 | 228 | login(): Promise{ 229 | return new Promise((resolve, reject) => { 230 | this.rpc.call('login', {login: this.options.transportConfig.login, passwd: this.options.transportConfig.passwd}, (data: {sessid:string}) => { 231 | this.sessid = data.sessid 232 | this.logged_in = true 233 | resolve(data) 234 | }, (data: Object) => { 235 | reject(data) 236 | }) 237 | }) 238 | } 239 | 240 | call(tracks: Array, destination: string, options?:VertoCallOptions): VertoCall { 241 | let call = new VertoCall(this.options.rtcConfig, this.rpc, destination, generateGUID(), options, this.options.ice_timeout, this.options.debug) 242 | 243 | for(let track of tracks) call.addTrack(track) 244 | this.calls[call.id] = call 245 | return call 246 | } 247 | 248 | isLogged(): boolean { 249 | return this.logged_in 250 | } 251 | 252 | logout(){ 253 | if (this.calls) { 254 | Object.keys(this.calls).forEach( 255 | key => { 256 | this.calls[key].hangup() 257 | } 258 | ) 259 | } 260 | this.rpc.close() 261 | this.rpc = null 262 | this.sessid = null 263 | this.logged_in = false 264 | } 265 | 266 | } 267 | 268 | export { Verto, CallDirection } 269 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vertojs 2 | 3 | Verto (VER-to) RTC is a FreeSWITCH endpoint that implements a subset of a JSON-RPC connection designed for use over secure websockets. The initial target is WebRTC to simplify coding and implementing calls from web browsers and devices to FreeSWITCH. This allows a web browser or other WebRTC client to originate a call using Verto into a FreeSWITCH installation and then out to the PSTN using SIP, SS7, or other supported protocol. 4 | 5 | This is a zero-dependency implementation that is no need to include jquery as in an original one. It doesn't contain any html stuff inside or media handlers as well. You should 6 | take care of fetch media tracks yourself (I think it is better not to hide useful features of you, browser provides a great API to handle media) 7 | 8 | ## Status 9 | 10 | This is a work in progress code. However, it is stable enough to use basic functions (calls). 11 | 12 | Pull requests are welcomed. 13 | 14 | 15 | ## Get started 16 | 17 | Package directory content 18 | 19 | 20 | **/dist** — contains a minified bundle exporting **Verto** symbol to a global namespace 21 | 22 | **/src** — contains source Typescript files 23 | 24 | To use this package you can either include *dist/verto.js* as a html <script> tag or import it using webpack like that 25 | 26 | ```typescript 27 | import { Verto } from 'vertojs' 28 | ``` 29 | 30 | Check index.html in the package directory to find out how to use this code with a html <script> tag 31 | 32 | ## Create a client instance 33 | 34 | ```typescript 35 | let verto = new Verto(options: VertoOptions) 36 | ``` 37 | 38 | ```typescript 39 | interface VertoOptions { 40 | transportConfig : JsonRpcClientParams 41 | // Verto transport configuration, check below 42 | 43 | rtcConfig? : RTCConfiguration 44 | // RTCConfiguration object, as described here 45 | // https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration 46 | // The most important thing is iceServers item that should be set to go over NAT 47 | 48 | debug? : boolean 49 | // Set true to get some useful debug info in browser console 50 | 51 | ice_timeout? : number 52 | // Milliseconds to stop waiting for ice candidates, default to 3000ms 53 | } 54 | 55 | interface JsonRpcClientParams { 56 | socketUrl : string 57 | // The URL where the verto interface lives 58 | // wss://server.example.com:8082 59 | 60 | login : string 61 | passwd : string 62 | } 63 | ``` 64 | 65 | ### Receive calls 66 | 67 | You should register to verto to receive calls. 68 | 69 | The following code is a simplified example of using the handler function to auto answer the first incoming call and add first received audio track to some <video> element. 70 | ```typescript 71 | try { 72 | let data = await verto.login() 73 | } catch (error) { 74 | alert("Access denied") 75 | return 76 | } 77 | 78 | let local_stream = await navigator.mediaDevices.getUserMedia({audio:true}) 79 | 80 | verto.subscribeEvent('invite', call => { 81 | 82 | call.subscribeEvent('track', (track) => { 83 | if(track.kind!='audio') return 84 | 85 | let stream = new MediaStream() 86 | stream.addTrack(track) 87 | 88 | let el = document.getElementById('video') 89 | el.srcObject = stream 90 | }) 91 | 92 | call.answer(local_stream.getTracks()) 93 | }) 94 | ``` 95 | 96 | ### Place calls 97 | 98 | 99 | ```typescript 100 | let local_stream = await navigator.mediaDevices.getUserMedia({audio:true}) 101 | 102 | let call = verto.call(local_stream.getTracks(), "9664") 103 | 104 | call.subscribeEvent('track', (track) => { 105 | if(track.kind!='audio') return 106 | 107 | let stream = new MediaStream() 108 | stream.addTrack(track) 109 | 110 | let el = document.getElementById('video') 111 | el.srcObject = stream 112 | }) 113 | ``` 114 | 115 | # API description 116 | 117 | There's a number (pretty small number) of Classes and Interfaces provided. 118 | 119 | ## Verto 120 | 121 | ### Methods 122 | 123 | **constructor** 124 | 125 | ```typescript 126 | let verto = new Verto(options: VertoOptions) 127 | ``` 128 | 129 | ```typescript 130 | interface VertoOptions { 131 | transportConfig : JsonRpcClientParams 132 | // Verto transport configuration, check below 133 | 134 | rtcConfig? : RTCConfiguration 135 | // RTCConfiguration object, as described here 136 | // https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration 137 | // The most important thing is iceServers item that should be set to go over NAT 138 | 139 | debug? : boolean 140 | // Set true to get some useful debug info in browser console 141 | 142 | ice_timeout? : number 143 | // Milliseconds to stop waiting for ice candidates, default to 3000ms 144 | } 145 | 146 | interface JsonRpcClientParams { 147 | socketUrl : string 148 | // The URL where the verto interface lives 149 | // wss://server.example.com:8082 150 | 151 | login : string 152 | passwd : string 153 | } 154 | ``` 155 | #### login 156 | 157 | ***Parameters*** 158 | 159 | - None 160 | 161 | ***Returns*** 162 | 163 | *Promise*, that will be resolved if the login process succeed or threw an exception otherwise. 164 | 165 | ```typescript 166 | verto.login() 167 | ``` 168 | 169 | #### **call** 170 | 171 | ***Parameters*** 172 | 173 | - tracks: Array<[MediaStreamTrack](https://developer.mozilla.org/ru/docs/Web/API/MediaStreamTrack)> 174 |
represents tracks to be sent to the remote call side 175 | - destination: string 176 |
an extension to be dialed 177 | - options?: [VertoCallOptions](#VertoCallOptions) 178 |
call options 179 | 180 | ***Returns*** 181 | 182 | - [VertoCall](#VertoCall) instance 183 | 184 | ```typescript 185 | let call = verto.call(tracks, destination, options) 186 | ``` 187 | 188 | #### **isLogged** 189 | 190 | ***Parameters*** 191 | 192 | - None 193 | 194 | ***Returns*** 195 | 196 | - Boolean 197 | 198 | ```typescript 199 | let isLogged = verto.isLogged() 200 | ``` 201 | 202 | #### **logout** 203 | 204 | ***Parameters*** 205 | 206 | - None 207 | 208 | ***Returns*** 209 | 210 | - Void 211 | 212 | ```typescript 213 | verto.logout() 214 | ``` 215 | ### Events 216 | 217 | #### invite 218 | 219 | Fires on incoming call. As a parameter handler will receive a [VertoCall](#VertoCall) instance. 220 | 221 | ```typescript 222 | verto.subscribeEvent('invite', call => { 223 | 224 | call.subscribeEvent('track', (track) => { 225 | if(track.kind!='audio') return 226 | 227 | let stream = new MediaStream() 228 | stream.addTrack(track) 229 | 230 | let el = document.getElementById('video') 231 | el.srcObject = stream 232 | }) 233 | 234 | call.answer(local_stream.getTracks()) 235 | }) 236 | ``` 237 | 238 | ## VertoCall 239 | 240 | This class instances should never be built manually, but using verto.call or incoming call handler. 241 | 242 | ### Methods 243 | 244 | #### **answer** 245 | 246 | ***Parameters*** 247 | 248 | - tracks: Array<[MediaStreamTrack](https://developer.mozilla.org/ru/docs/Web/API/MediaStreamTrack)> 249 |
represents tracks to be sent to the remote call side 250 | 251 | ***Returns*** 252 | 253 | - None 254 | 255 | ```typescript 256 | call.answer(tracks) 257 | ``` 258 | #### **hangup** 259 | 260 | ***Parameters*** 261 | 262 | - None 263 | 264 | ***Returns*** 265 | 266 | - None 267 | 268 | ```typescript 269 | call.hangup() 270 | ``` 271 | 272 | #### **dtmf** 273 | 274 | ***Parameters*** 275 | 276 | - String 277 | 278 | ***Returns*** 279 | 280 | - None 281 | 282 | ```typescript 283 | call.dtmf('5') 284 | ``` 285 | 286 | #### **hold** 287 | 288 | ***Parameters*** 289 | 290 | - None 291 | 292 | ***Returns*** 293 | 294 | - None 295 | 296 | ```typescript 297 | call.hold() 298 | ``` 299 | 300 | #### **unhold** 301 | 302 | ***Parameters*** 303 | 304 | - None 305 | 306 | ***Returns*** 307 | 308 | - None 309 | 310 | ```typescript 311 | call.unhold() 312 | ``` 313 | 314 | #### **toggleHold** 315 | 316 | ***Parameters*** 317 | 318 | - None 319 | 320 | ***Returns*** 321 | 322 | - None 323 | 324 | ```typescript 325 | call.toggleHold() 326 | ``` 327 | 328 | #### **getStats** 329 | 330 | ***Parameters*** 331 | 332 | - None 333 | 334 | ***Returns*** 335 | 336 | https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/getStats 337 | 338 | ### Instance variables 339 | 340 | #### **id** 341 | 342 | - *String* — the call id 343 | 344 | #### **options** 345 | 346 | - *[VertoCallOptions](#VertoCallOptions)* 347 | 348 | #### **direction** 349 | 350 | - *[CallDirection](#CallDirection)* 351 | 352 | 353 | ### Events 354 | 355 | #### answer 356 | 357 | Fires when the call is answered. 358 | 359 | ```typescript 360 | call.subscribeEvent('answer', () => { 361 | // Do something on answer 362 | }) 363 | ``` 364 | 365 | #### track 366 | 367 | Fires when a MediaStreamTrack is received 368 | 369 | ```typescript 370 | verto.subscribeEvent('invite', call => { 371 | 372 | call.subscribeEvent('track', (track) => { 373 | if(track.kind!='audio') return 374 | 375 | let stream = new MediaStream() 376 | stream.addTrack(track) 377 | 378 | let el = document.getElementById('video') 379 | el.srcObject = stream 380 | }) 381 | 382 | call.answer(local_stream.getTracks()) 383 | }) 384 | ``` 385 | 386 | #### bye 387 | 388 | Fires when the call is ended. 389 | 390 | ```typescript 391 | call.subscribeEvent('bye', cause => { 392 | // Do something on call end 393 | }) 394 | ``` 395 | 396 | ## Interfaces 397 | 398 | #### VertoCallOptions 399 | 400 | ```typescript 401 | interface VertoCallOptions { 402 | caller_id_number? : string 403 | caller_id_name? : string 404 | callee_id_number? : string 405 | callee_id_name? : string 406 | } 407 | ``` 408 | 409 | #### VertoOptions 410 | ```typescript 411 | interface VertoOptions { 412 | transportConfig : JsonRpcClientParams 413 | // Verto transport configuration, check below 414 | 415 | rtcConfig? : RTCConfiguration 416 | // RTCConfiguration object, as described here 417 | // https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration 418 | // The most important thing is iceServers item that should be set to go over NAT 419 | 420 | debug? : boolean 421 | // Set true to get some useful debug info in browser console 422 | 423 | ice_timeout? : number 424 | // Milliseconds to stop waiting for ice candidates, default to 3000ms 425 | } 426 | ``` 427 | 428 | #### JsonRpcClientParams 429 | ```typescript 430 | interface JsonRpcClientParams { 431 | socketUrl : string 432 | // The URL where the verto interface lives 433 | // wss://server.example.com:8082 434 | 435 | login : string 436 | passwd : string 437 | } 438 | ``` 439 | 440 | #### CallDirection 441 | ```typescript 442 | enum CallDirection { 443 | Incoming, 444 | Outgoing 445 | } 446 | ``` 447 | 448 | ## Event handling 449 | 450 | Both [Verto](#Verto) and [VertoCall](#VertoCall) classes uses the same event handling system. 451 | 452 | **subscribeEvent** 453 | 454 | ***Parameters*** 455 | 456 | - name : string 457 | - handler : {(data:any):void} 458 | 459 | ***Returns*** 460 | 461 | - *String* identifies the handler 462 | 463 | ```typescript 464 | let handler_id = verto.subscribeEvent(name, handler) 465 | ``` 466 | 467 | **unsubscribeEvent** 468 | 469 | ***Parameters*** 470 | 471 | - name : string 472 | - handler_id? : string 473 |
if ommited, all the handlers for *name* event will be deleted 474 | 475 | ***Returns*** 476 | 477 | - None 478 | 479 | ```typescript 480 | verto.unsubscribeEvent(name, handler_id) 481 | ``` 482 | 483 | ## License 484 | 485 | Copyright (c) 2019–2022 Roman Yerin <r.yerin@ion.team> 486 | 487 | Licensed under the 3-clause BSD license. 488 | 489 | -------------------------------------------------------------------------------- /stats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | RollUp Visualizer 9 | 146 | 147 | 148 |
149 | 3260 | 3277 | 3278 | 3279 | 3280 | --------------------------------------------------------------------------------