@94ai/softphone 5.0.11 → 5.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/html-softphone-demo/WebrtcDiver.js +320 -0
- package/html-softphone-demo/embed-ui/index.html +99 -0
- package/html-softphone-demo/{index-embed.html → embed-ui/softphone.js} +24 -214
- package/html-softphone-demo/embed-ui/util.js +123 -0
- package/html-softphone-demo/index-channel.html +904 -0
- package/html-softphone-demo/index-local.html +3 -10
- package/html-softphone-demo/index-switch-pcm.html +613 -0
- package/html-softphone-demo/{index-other.html → index-test.html} +10 -3
- package/html-softphone-demo/index.html +73 -54
- package/html-softphone-demo/micro-call-ui/CryptoJS.js +3 -0
- package/html-softphone-demo/micro-call-ui/index.html +102 -0
- package/html-softphone-demo/micro-call-ui/microphone.js +829 -0
- package/html-softphone-demo/micro-call-ui/qiankun.js +2 -0
- package/html-softphone-demo/micro-call-ui/util.js +17 -0
- package/html-softphone-demo/micro-call-ui/wujie.js +3 -0
- package/html-softphone-demo/pcm-server/16bit-44100.pcm +0 -0
- package/html-softphone-demo/pcm-server/index.html +110 -0
- package/html-softphone-demo/pcm-server/server.js +54 -0
- package/html-softphone-demo/softphone.umd.min.js +2 -2
- package/html-softphone-demo.7z +0 -0
- package/lib/index.d.ts +1 -1
- package/lib/softphone.cjs.min.cjs +1 -1
- package/lib/softphone.esm-bundler.min.mjs +1 -1
- package/lib/softphone.umd.min.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
const TYPED_ARRAYS = {
|
|
2
|
+
'8bitInt': Int8Array,
|
|
3
|
+
'16bitInt': Int16Array,
|
|
4
|
+
'32bitInt': Int32Array,
|
|
5
|
+
'32bitFloat': Float32Array
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const ENCODINGS = {
|
|
9
|
+
'8bitInt': 128,
|
|
10
|
+
'16bitInt': 32768,
|
|
11
|
+
'32bitInt': 2147483648,
|
|
12
|
+
'32bitFloat': 1
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const isTypedArray = (data) => {
|
|
16
|
+
return (data.byteLength && data.buffer && data.buffer.constructor === ArrayBuffer)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const getFormatedValue = (value, encoding) => {
|
|
20
|
+
const data = new (TYPED_ARRAYS[encoding] || TYPED_ARRAYS['16bitInt'])(value.buffer)
|
|
21
|
+
// console.log('data', value.length, value.buffer, data)
|
|
22
|
+
const float32 = new Float32Array(value.length)
|
|
23
|
+
let i
|
|
24
|
+
|
|
25
|
+
for (i = 0; i < data.length; i++) {
|
|
26
|
+
float32[i] = data[i] / (ENCODINGS[encoding] || ENCODINGS['16bitInt'])
|
|
27
|
+
}
|
|
28
|
+
return float32
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class WebrtcDiver {
|
|
32
|
+
static normalRtc = window.RTCPeerConnection
|
|
33
|
+
static normalAddTrack = window.RTCPeerConnection.prototype.addTrack
|
|
34
|
+
static normalAddStream = window.RTCPeerConnection.prototype.addStream
|
|
35
|
+
static writers = []
|
|
36
|
+
static stream
|
|
37
|
+
static track
|
|
38
|
+
static rtcList = []
|
|
39
|
+
static ws
|
|
40
|
+
static audioTrackGenerator
|
|
41
|
+
|
|
42
|
+
options = {
|
|
43
|
+
encoding: '16bitInt',
|
|
44
|
+
channels: 2,
|
|
45
|
+
sampleRate: 44100,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
audioCtx
|
|
49
|
+
gainNode
|
|
50
|
+
samples
|
|
51
|
+
requestId
|
|
52
|
+
startTime
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
constructor(option = {}) {
|
|
56
|
+
this.options = {
|
|
57
|
+
...this.options,
|
|
58
|
+
...option
|
|
59
|
+
}
|
|
60
|
+
this.samples = new Float32Array()
|
|
61
|
+
this.flush = this.flush.bind(this)
|
|
62
|
+
this.requestId = requestAnimationFrame(this.flush)
|
|
63
|
+
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)()
|
|
64
|
+
// context needs to be resumed on iOS and Safari (or it will stay in "suspended" state)
|
|
65
|
+
this.audioCtx.resume()
|
|
66
|
+
this.audioCtx.onstatechange = () => console.log(this.audioCtx.state) // if you want to see "Running" state in console and be happy about it
|
|
67
|
+
this.gainNode = this.audioCtx.createGain()
|
|
68
|
+
this.gainNode.gain.value = 1
|
|
69
|
+
this.gainNode.connect(this.audioCtx.destination)
|
|
70
|
+
this.startTime = this.audioCtx.currentTime
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
feed(data) {
|
|
74
|
+
if (!isTypedArray(data)) return
|
|
75
|
+
data = getFormatedValue(data, this.options.encoding)
|
|
76
|
+
const tmp = new Float32Array(this.samples.length + data.length)
|
|
77
|
+
tmp.set(this.samples, 0)
|
|
78
|
+
tmp.set(data, this.samples.length)
|
|
79
|
+
this.samples = tmp
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
volume(volume) {
|
|
83
|
+
this.gainNode.gain.value = volume
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
destroy() {
|
|
87
|
+
try {
|
|
88
|
+
cancelAnimationFrame(this.requestId)
|
|
89
|
+
this.requestId = null
|
|
90
|
+
this.samples = null
|
|
91
|
+
this.audioCtx.close()
|
|
92
|
+
this.audioCtx = null
|
|
93
|
+
this.gainNode = null
|
|
94
|
+
this.startTime = null
|
|
95
|
+
WebrtcDiver.writers = []
|
|
96
|
+
WebrtcDiver.rtcList = []
|
|
97
|
+
WebrtcDiver.stream = null
|
|
98
|
+
WebrtcDiver.track = null
|
|
99
|
+
this.microphoneTransfer()
|
|
100
|
+
document.getElementById('pcm-button-player').removeEventListener('click', this.arraybufferTransfer)
|
|
101
|
+
document.getElementById('ordinary-microphone').removeEventListener('click', this.microphoneTransfer)
|
|
102
|
+
} catch (e) {
|
|
103
|
+
// console.log(e)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
flush() {
|
|
108
|
+
if (this.samples.length) {
|
|
109
|
+
// const bufferSource = this.audioCtx.createBufferSource()
|
|
110
|
+
const length = this.samples.length / this.options.channels
|
|
111
|
+
const audioBuffer = this.audioCtx.createBuffer(this.options.channels, length, this.options.sampleRate)
|
|
112
|
+
let audioData
|
|
113
|
+
let channel
|
|
114
|
+
let offset
|
|
115
|
+
let i
|
|
116
|
+
let decrement
|
|
117
|
+
|
|
118
|
+
for (channel = 0; channel < this.options.channels; channel++) {
|
|
119
|
+
audioData = audioBuffer.getChannelData(channel)
|
|
120
|
+
offset = channel
|
|
121
|
+
decrement = 50
|
|
122
|
+
for (i = 0; i < length; i++) {
|
|
123
|
+
audioData[i] = this.samples[offset]
|
|
124
|
+
/* fadein */
|
|
125
|
+
if (i < 50) {
|
|
126
|
+
audioData[i] = (audioData[i] * i) / 50
|
|
127
|
+
}
|
|
128
|
+
/* fadeout*/
|
|
129
|
+
if (i >= (length - 51)) {
|
|
130
|
+
audioData[i] = (audioData[i] * decrement--) / 50
|
|
131
|
+
}
|
|
132
|
+
offset += this.options.channels
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (this.startTime < this.audioCtx.currentTime) {
|
|
137
|
+
this.startTime = this.audioCtx.currentTime
|
|
138
|
+
}
|
|
139
|
+
// bufferSource.buffer = audioBuffer
|
|
140
|
+
// bufferSource.connect(this.gainNode)
|
|
141
|
+
// bufferSource.start(this.startTime)
|
|
142
|
+
this.startTime += audioBuffer.duration
|
|
143
|
+
this.samples = new Float32Array()
|
|
144
|
+
|
|
145
|
+
// 创建 AudioData 对象
|
|
146
|
+
const voiceData = new window.AudioData({
|
|
147
|
+
format: 'f32-planar',
|
|
148
|
+
sampleRate: audioBuffer.sampleRate, // 采样率
|
|
149
|
+
numberOfFrames: audioBuffer.length / audioBuffer.numberOfChannels, // 每帧样本数
|
|
150
|
+
numberOfChannels: audioBuffer.numberOfChannels, // 声道
|
|
151
|
+
timestamp: this.audioCtx.currentTime * 1e6,
|
|
152
|
+
data: audioData
|
|
153
|
+
})
|
|
154
|
+
WebrtcDiver.writers.forEach(item => {
|
|
155
|
+
item.write(voiceData)
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
this.requestId = requestAnimationFrame(this.flush)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
static refreshWriters() {
|
|
162
|
+
WebrtcDiver.writers = []
|
|
163
|
+
WebrtcDiver.audioTrackGenerator = new MediaStreamTrackGenerator({ kind: 'audio' })
|
|
164
|
+
const writableStream = WebrtcDiver.audioTrackGenerator.writable
|
|
165
|
+
const writer = writableStream.getWriter()
|
|
166
|
+
WebrtcDiver.writers.push(writer)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
addTrack() {
|
|
170
|
+
console.log('调用addTrack拦截: ', arguments)
|
|
171
|
+
WebrtcDiver.refreshWriters()
|
|
172
|
+
WebrtcDiver.normalAddTrack.apply(this, [WebrtcDiver.audioTrackGenerator])
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
addStream() {
|
|
176
|
+
console.log('调用addStream拦截: ', arguments)
|
|
177
|
+
if (WebrtcDiver.stream) {
|
|
178
|
+
console.log('设置指定麦克风: ', WebrtcDiver.stream)
|
|
179
|
+
arguments[0] = WebrtcDiver.track
|
|
180
|
+
}
|
|
181
|
+
WebrtcDiver.normalAddStream.apply(this, arguments)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
resetWriteAdd() {
|
|
185
|
+
window.RTCPeerConnection.prototype.addTrack = this.addTrack
|
|
186
|
+
window.RTCPeerConnection.prototype.addStream = this.addStream
|
|
187
|
+
if (window.webkitRTCPeerConnection) {
|
|
188
|
+
window.webkitRTCPeerConnection.prototype.addTrack = this.addTrack
|
|
189
|
+
window.webkitRTCPeerConnection.prototype.addTrack = this.addStream
|
|
190
|
+
}
|
|
191
|
+
console.log('重写 -> addTrack & addStream -> 完成')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
monitorResetRtcAdd() {
|
|
195
|
+
Object.defineProperty(window.RTCPeerConnection.prototype, 'addStream', {
|
|
196
|
+
get: () => {
|
|
197
|
+
console.log('监听到 addStream 被获取')
|
|
198
|
+
return this.addStream
|
|
199
|
+
},
|
|
200
|
+
set: (f) => {
|
|
201
|
+
if (this.addStream.toString() !== f.toString()) {
|
|
202
|
+
console.log('监听到第三方重写addStream, 继续重写')
|
|
203
|
+
WebrtcDiver.normalAddStream = f
|
|
204
|
+
window.RTCPeerConnection.prototype.addStream = this.addStream
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
Object.defineProperty(window.RTCPeerConnection.prototype, 'addTrack', {
|
|
209
|
+
get: () => {
|
|
210
|
+
console.log('监听到 addTrack 被获取')
|
|
211
|
+
return this.addTrack
|
|
212
|
+
},
|
|
213
|
+
set: (f) => {
|
|
214
|
+
if (this.addTrack.toString() !== f.toString()) {
|
|
215
|
+
console.log('监听到第三方重写addTrack, 继续重写')
|
|
216
|
+
WebrtcDiver.normalAddTrack = f
|
|
217
|
+
window.RTCPeerConnection.prototype.addTrack = this.addTrack
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
console.log('开启重写监听 -> addStream & addTrack')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
monitorResetRtcPrototype() {
|
|
225
|
+
WebrtcDiver.normalRtc = new Proxy(WebrtcDiver.normalRtc, {
|
|
226
|
+
get: (target, key) => {
|
|
227
|
+
return Reflect.get(target, key)
|
|
228
|
+
},
|
|
229
|
+
set: (target, key, value) => {
|
|
230
|
+
if (key === 'prototype') {
|
|
231
|
+
console.log('监听到 RTCPeerConnection.prototype 被重写, 继续重写')
|
|
232
|
+
const result = Reflect.set(target, key, value)
|
|
233
|
+
this.monitorResetRtcAdd()
|
|
234
|
+
return result
|
|
235
|
+
} else {
|
|
236
|
+
return Reflect.set(target, key, value)
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
construct: function(target, otherArray) {
|
|
240
|
+
const that = new target(...otherArray)
|
|
241
|
+
WebrtcDiver.rtcList.push(that)
|
|
242
|
+
console.log('监听到RTC创建: ', that)
|
|
243
|
+
return that
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
console.log('开启重写监听 -> RTCPeerConnection.prototype')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
monitorResetRtc() {
|
|
250
|
+
Object.defineProperty(window, 'RTCPeerConnection', {
|
|
251
|
+
get: () => {
|
|
252
|
+
return WebrtcDiver.normalRtc
|
|
253
|
+
},
|
|
254
|
+
set: (f) => {
|
|
255
|
+
if (WebrtcDiver.normalRtc.toString() !== f.toString()) {
|
|
256
|
+
console.log('监听到第三方重写RTCPeerConnection, 继续重写')
|
|
257
|
+
WebrtcDiver.normalRtc = f
|
|
258
|
+
this.monitorResetRtcPrototype()
|
|
259
|
+
this.monitorResetRtcAdd()
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
console.log('开启重写监听 -> RTCPeerConnection')
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
openPcmData() {
|
|
267
|
+
WebrtcDiver.ws = new WebSocket('ws://127.0.0.1:8899');
|
|
268
|
+
WebrtcDiver.ws.binaryType = 'arraybuffer';
|
|
269
|
+
WebrtcDiver.ws.addEventListener('message', (event) => {
|
|
270
|
+
// 可以传 ArrayBuffer 或者 任意TypedArray
|
|
271
|
+
this.feed(new Uint16Array(event.data));
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
arraybufferTransfer = async () => {
|
|
276
|
+
this.openPcmData()
|
|
277
|
+
const sender = WebrtcDiver.rtcList[WebrtcDiver.rtcList.length - 1].getSenders().find(sender => sender.track.kind === 'audio');
|
|
278
|
+
if (sender) {
|
|
279
|
+
sender.track.stop()
|
|
280
|
+
WebrtcDiver.refreshWriters()
|
|
281
|
+
await sender.replaceTrack(WebrtcDiver.audioTrackGenerator);
|
|
282
|
+
}
|
|
283
|
+
document.getElementById('pcm-button-player').disabled = true
|
|
284
|
+
document.getElementById('ordinary-microphone').disabled = false
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
microphoneTransfer = async () => {
|
|
288
|
+
try {
|
|
289
|
+
if (WebrtcDiver.ws) {
|
|
290
|
+
WebrtcDiver.ws.close(1000, 'Normal closure');
|
|
291
|
+
WebrtcDiver.ws.onopen = null;
|
|
292
|
+
WebrtcDiver.ws.onmessage = null;
|
|
293
|
+
WebrtcDiver.ws.onerror = null;
|
|
294
|
+
WebrtcDiver.ws.onclose = null;
|
|
295
|
+
WebrtcDiver.ws = null
|
|
296
|
+
this.samples = new Float32Array()
|
|
297
|
+
const localStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
298
|
+
const audioTrack = localStream.getAudioTracks()[0];
|
|
299
|
+
const sender = WebrtcDiver.rtcList[WebrtcDiver.rtcList.length - 1].getSenders().find(sender => sender.track.kind === 'audio');
|
|
300
|
+
if (sender) {
|
|
301
|
+
sender.track.stop()
|
|
302
|
+
await sender.replaceTrack(audioTrack);
|
|
303
|
+
}
|
|
304
|
+
document.getElementById('ordinary-microphone').disabled = true
|
|
305
|
+
document.getElementById('pcm-button-player').disabled = false
|
|
306
|
+
}
|
|
307
|
+
} catch (e) {
|
|
308
|
+
// console.log(e)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async init() {
|
|
313
|
+
this.resetWriteAdd()
|
|
314
|
+
this.monitorResetRtcAdd()
|
|
315
|
+
this.monitorResetRtcPrototype()
|
|
316
|
+
this.monitorResetRtc()
|
|
317
|
+
document.getElementById('pcm-button-player').addEventListener('click', this.arraybufferTransfer)
|
|
318
|
+
document.getElementById('ordinary-microphone').addEventListener('click', this.microphoneTransfer)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport"
|
|
6
|
+
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
|
|
7
|
+
<title>softphone</title>
|
|
8
|
+
<style>
|
|
9
|
+
.nf-transparent {
|
|
10
|
+
visibility: hidden;
|
|
11
|
+
}
|
|
12
|
+
.nf-softphone-text {
|
|
13
|
+
text-align: center;
|
|
14
|
+
line-height: 32px;
|
|
15
|
+
margin-top: -32px;
|
|
16
|
+
}
|
|
17
|
+
.nf-softphone-container {
|
|
18
|
+
margin-left: 10px;
|
|
19
|
+
height: 32px;
|
|
20
|
+
min-width: 600px;
|
|
21
|
+
}
|
|
22
|
+
.nf-softphone-iframe-container {
|
|
23
|
+
height: 32px;
|
|
24
|
+
width: 100%;
|
|
25
|
+
overflow: visible;
|
|
26
|
+
}
|
|
27
|
+
.nf-softphone-iframe {
|
|
28
|
+
width: 100vw;
|
|
29
|
+
height: 100vh;
|
|
30
|
+
top: 0;
|
|
31
|
+
left: 0;
|
|
32
|
+
}
|
|
33
|
+
</style>
|
|
34
|
+
</head>
|
|
35
|
+
<body>
|
|
36
|
+
<div id='softpone-context1' style='overflow: hidden'>
|
|
37
|
+
<div style="display: flex;height: 32px;">
|
|
38
|
+
<div style="min-width: 600px;width: 600px;" id='softphone1'></div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<script src="./util.js"></script>
|
|
42
|
+
<script src="./softphone.js"></script>
|
|
43
|
+
<script>
|
|
44
|
+
// 国内
|
|
45
|
+
const agentTag1 = 'zoujh-test'
|
|
46
|
+
const appKey1 = '032d44009bff1752'
|
|
47
|
+
const appSecret1 = '4c7304c94c5e8725613516d4d6db679b'
|
|
48
|
+
// 海外
|
|
49
|
+
// const agentId = '2357'
|
|
50
|
+
// const appKey1 = 'a52e61f0d9ff1527'
|
|
51
|
+
// const appSecret1 = '7ed1d78fbe214ba8b8e6a2dc9c6955aa'
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
👈 拿到实例后可以手动触发软电话实例的各种动作,如签入,签出,接听,忽略,挂断等等,注意这个实例不是软电话实例,是链接软电话通讯的实例
|
|
55
|
+
*/
|
|
56
|
+
const nfSoftPhone1 = (new SoftphoneManager()).initSoftphone({ // 👈 当多实例时使用类创建新实例
|
|
57
|
+
el: '#softphone1', // 👈 软电话容器
|
|
58
|
+
selector: '#softpone-context1', // 👈 软电话容器查询上下文,用于多实例隔离软电话dom的查询环境
|
|
59
|
+
ancestorOrigin: 'nf-softphone1', // origin,ancestorOrigin,destinationOrigin具体含义参见下文,如果是单实例,可以写死origin=ai-softphone&destinationOrigin=nf-softphone&ancestorOrigin=nf-softphone
|
|
60
|
+
destinationOrigin: 'nf-softphone1',
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* start - 海外特有参数
|
|
64
|
+
*/
|
|
65
|
+
// sgGateway: '1',
|
|
66
|
+
// sgBase: '1',
|
|
67
|
+
// sgOpen: '1',
|
|
68
|
+
// sgDomain: '1',
|
|
69
|
+
// env: 'sg',
|
|
70
|
+
/**
|
|
71
|
+
* end - 海外特有参数
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
// agentId, // 👈 坐席唯一标志
|
|
75
|
+
agentTag: agentTag1, // 👈 坐席唯一标志
|
|
76
|
+
appKey: appKey1, // 👈 企业appKey
|
|
77
|
+
appSecret: appSecret1,
|
|
78
|
+
extStatus: '1', // 👈 初始化是否处于小休状态, 非1 小休 1 在线
|
|
79
|
+
|
|
80
|
+
softphoneConnectCallBack: (data) => {
|
|
81
|
+
console.log('softphone-connect')
|
|
82
|
+
}, // 👈 签入签出回调
|
|
83
|
+
softphoneCallRefreshCallBack: (data) => {console.log('softphone-call-refresh')}, // 👈 来电刷新来电记录列表回调
|
|
84
|
+
softphoneSeatStatusChangeCallBack: (data) => {console.log('softphone-seats-status-change')},// 👈 小休和在线切换回调
|
|
85
|
+
softphoneAcceptCallBack: (data) => {console.log('softphone-accept')},// 👈 接听回调
|
|
86
|
+
softphoneIgnoreCallBack: (data) => {console.log('softphone-ignore')},// 👈 忽略回调
|
|
87
|
+
softphoneHangupCallBack: (data) => {console.log('softphone-hangup')},// 👈 挂断回调
|
|
88
|
+
softphoneSessionStateChangeCallBack: (data) => {console.log('softphone-session-state-change')},// 👈 会话状态改变回调
|
|
89
|
+
softphoneIncomingCallBack: (data) => {console.log('softphone-incoming')},// 👈 来电回调
|
|
90
|
+
softphoneSendDtmfCallBack: (data) => {console.log('softphone-send-dtmf')},// 👈 转人工回调
|
|
91
|
+
softphoneConnectRegisteredCallBack: (data) => {console.log('softphone-connect-registered')},// 👈 动态绑定动作完成回调(在这之后才能执行动作类总线通讯)
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// setTimeout(() => {
|
|
95
|
+
// nfSoftPhone1.destroy()
|
|
96
|
+
// }, 3000)
|
|
97
|
+
</script>
|
|
98
|
+
</body>
|
|
99
|
+
</html>
|
|
@@ -1,170 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport"
|
|
6
|
-
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
|
|
7
|
-
<title>softphone</title>
|
|
8
|
-
<style>
|
|
9
|
-
.nf-transparent {
|
|
10
|
-
visibility: hidden;
|
|
11
|
-
}
|
|
12
|
-
.nf-softphone-text {
|
|
13
|
-
text-align: center;
|
|
14
|
-
line-height: 32px;
|
|
15
|
-
margin-top: -32px;
|
|
16
|
-
}
|
|
17
|
-
.nf-softphone-container {
|
|
18
|
-
margin-left: 10px;
|
|
19
|
-
height: 32px;
|
|
20
|
-
min-width: 600px;
|
|
21
|
-
}
|
|
22
|
-
.nf-softphone-iframe-container {
|
|
23
|
-
height: 32px;
|
|
24
|
-
width: 100%;
|
|
25
|
-
overflow: visible;
|
|
26
|
-
}
|
|
27
|
-
.nf-softphone-iframe {
|
|
28
|
-
width: 100vw;
|
|
29
|
-
height: 100vh;
|
|
30
|
-
top: 0;
|
|
31
|
-
left: 0;
|
|
32
|
-
}
|
|
33
|
-
</style>
|
|
34
|
-
</head>
|
|
35
|
-
<body>
|
|
36
|
-
<div id='softpone-context1' style='overflow: hidden'>
|
|
37
|
-
<div style="display: flex;height: 32px;">
|
|
38
|
-
<div style="min-width: 600px;width: 600px;" id='softphone1'></div>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
<script>
|
|
42
|
-
function generateUUID() { // Public Domain/MIT
|
|
43
|
-
let d = Date.now();
|
|
44
|
-
if (typeof performance !== 'undefined' && typeof performance.now === 'function'){
|
|
45
|
-
d += performance.now(); //use high-precision timer if available
|
|
46
|
-
}
|
|
47
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
48
|
-
const r = (d + Math.random() * 16) % 16 | 0;
|
|
49
|
-
d = Math.floor(d / 16);
|
|
50
|
-
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function debounce(func, wait, immediate) {
|
|
55
|
-
let timeout, args, context, timestamp, result
|
|
56
|
-
|
|
57
|
-
const later = function() {
|
|
58
|
-
// 据上一次触发时间间隔
|
|
59
|
-
const last = +new Date() - timestamp
|
|
60
|
-
|
|
61
|
-
// 上次被包装函数被调用时间间隔last小于设定时间间隔wait
|
|
62
|
-
if (last < wait && last > 0) {
|
|
63
|
-
timeout = setTimeout(later, wait - last)
|
|
64
|
-
} else {
|
|
65
|
-
timeout = null
|
|
66
|
-
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
|
|
67
|
-
if (!immediate) {
|
|
68
|
-
result = func.apply(context, args)
|
|
69
|
-
if (!timeout) context = args = null
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return function(...args) {
|
|
75
|
-
// @ts-ignore
|
|
76
|
-
context = this
|
|
77
|
-
timestamp = +new Date()
|
|
78
|
-
const callNow = immediate && !timeout
|
|
79
|
-
// 如果延时不存在,重新设定延时
|
|
80
|
-
if (!timeout) timeout = setTimeout(later, wait)
|
|
81
|
-
if (callNow) {
|
|
82
|
-
result = func.apply(context, args)
|
|
83
|
-
// @ts-ignore
|
|
84
|
-
context = args = null
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return result
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// 用于存储传入 nextTick 的回调函数
|
|
92
|
-
const callbacks = [];
|
|
93
|
-
// 标记是否已经安排了回调函数的执行
|
|
94
|
-
let pending = false;
|
|
95
|
-
|
|
96
|
-
// 执行队列中的所有回调函数
|
|
97
|
-
function flushCallbacks() {
|
|
98
|
-
pending = false;
|
|
99
|
-
// 复制一份当前的回调队列,避免在执行过程中添加新的回调影响循环
|
|
100
|
-
const copies = callbacks.slice(0);
|
|
101
|
-
callbacks.length = 0;
|
|
102
|
-
// 依次执行每个回调函数,并处理可能出现的异常
|
|
103
|
-
for (let i = 0; i < copies.length; i++) {
|
|
104
|
-
try {
|
|
105
|
-
copies[i]();
|
|
106
|
-
} catch (e) {
|
|
107
|
-
console.error('Error in nextTick callback:', e);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// 根据浏览器支持情况选择合适的异步执行方式
|
|
113
|
-
let timerFunc;
|
|
114
|
-
if (typeof Promise !== 'undefined') {
|
|
115
|
-
// 如果支持 Promise,使用 Promise 的 then 方法创建微任务
|
|
116
|
-
const p = Promise.resolve();
|
|
117
|
-
timerFunc = () => {
|
|
118
|
-
p.then(flushCallbacks);
|
|
119
|
-
};
|
|
120
|
-
} else if (typeof MutationObserver !== 'undefined') {
|
|
121
|
-
// 如果支持 MutationObserver,使用它创建微任务
|
|
122
|
-
let counter = 1;
|
|
123
|
-
const observer = new MutationObserver(flushCallbacks);
|
|
124
|
-
const textNode = document.createTextNode(String(counter));
|
|
125
|
-
observer.observe(textNode, {
|
|
126
|
-
characterData: true
|
|
127
|
-
});
|
|
128
|
-
timerFunc = () => {
|
|
129
|
-
counter = (counter + 1) % 2;
|
|
130
|
-
textNode.data = String(counter);
|
|
131
|
-
};
|
|
132
|
-
} else if (typeof setImmediate !== 'undefined') {
|
|
133
|
-
// 如果支持 setImmediate,使用它创建宏任务
|
|
134
|
-
timerFunc = () => {
|
|
135
|
-
setImmediate(flushCallbacks);
|
|
136
|
-
};
|
|
137
|
-
} else {
|
|
138
|
-
// 最后使用 setTimeout 作为兜底方案创建宏任务
|
|
139
|
-
timerFunc = () => {
|
|
140
|
-
setTimeout(flushCallbacks, 0);
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// 实现 nextTick 函数,支持传入回调函数或返回 Promise
|
|
145
|
-
async function nextTick(cb) {
|
|
146
|
-
return new Promise((resolve) => {
|
|
147
|
-
// 将回调函数和 resolve 函数封装成一个新的回调
|
|
148
|
-
callbacks.push(() => {
|
|
149
|
-
if (cb) {
|
|
150
|
-
try {
|
|
151
|
-
cb();
|
|
152
|
-
} catch (e) {
|
|
153
|
-
console.error('Error in nextTick callback:', e);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
// 当回调执行完毕后,resolve Promise
|
|
157
|
-
resolve(true);
|
|
158
|
-
});
|
|
159
|
-
// 如果当前没有正在执行的回调安排,安排一次新的执行
|
|
160
|
-
if (!pending) {
|
|
161
|
-
pending = true;
|
|
162
|
-
timerFunc();
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
class SoftphoneManager {
|
|
1
|
+
class SoftphoneManager {
|
|
168
2
|
|
|
169
3
|
id
|
|
170
4
|
timer
|
|
@@ -187,7 +21,7 @@ async function nextTick(cb) {
|
|
|
187
21
|
envMap: {
|
|
188
22
|
test: 'http://softphone.test.k8s.com',
|
|
189
23
|
dev: 'http://softphone.dev.k8s.com',
|
|
190
|
-
sg: 'https://
|
|
24
|
+
sg: 'https://seat.teleai.com',
|
|
191
25
|
gray: 'http://softphone.gray.94ai.com',
|
|
192
26
|
prod: 'https://seat.94ai.com',
|
|
193
27
|
},
|
|
@@ -407,7 +241,11 @@ async function nextTick(cb) {
|
|
|
407
241
|
Object.keys(this.option.instanceMap).forEach(key => {
|
|
408
242
|
const el = document.getElementById(key)
|
|
409
243
|
if (el && el.parentNode) {
|
|
410
|
-
|
|
244
|
+
if (el instanceof HTMLIFrameElement) {
|
|
245
|
+
el.remove()
|
|
246
|
+
} else {
|
|
247
|
+
el.parentNode.removeChild(el)
|
|
248
|
+
}
|
|
411
249
|
}
|
|
412
250
|
})
|
|
413
251
|
}
|
|
@@ -616,52 +454,24 @@ async function nextTick(cb) {
|
|
|
616
454
|
this.timer = undefined
|
|
617
455
|
}
|
|
618
456
|
|
|
619
|
-
destroy = () => {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
457
|
+
destroy = async () => {
|
|
458
|
+
return new Promise((resolve) => {
|
|
459
|
+
this.disconnect()
|
|
460
|
+
let timer = setTimeout(() => {
|
|
461
|
+
clearTimeout(timer)
|
|
462
|
+
timer = undefined
|
|
463
|
+
window.removeEventListener('message', this.handlingCommunication, false);
|
|
464
|
+
if (this.option.scrollview) {
|
|
465
|
+
document.querySelector(this.option.scrollview)?.removeEventListener('scroll', this.handlingScroll)
|
|
466
|
+
document.querySelector(this.option.scrollview)?.removeEventListener('scroll', this.handlingScrollTransparent)
|
|
467
|
+
}
|
|
468
|
+
this.removeCssCode()
|
|
469
|
+
this.handlerDomRemove()
|
|
470
|
+
this.removeTimer()
|
|
471
|
+
resolve()
|
|
472
|
+
})
|
|
473
|
+
})
|
|
628
474
|
}
|
|
629
475
|
}
|
|
630
476
|
|
|
631
477
|
const softphoneManager = new SoftphoneManager()
|
|
632
|
-
|
|
633
|
-
const agentTag1 = 'zoujh-test'
|
|
634
|
-
const appKey1 = '032d44009bff1752'
|
|
635
|
-
const appSecret1 = '4c7304c94c5e8725613516d4d6db679b'
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
👈 拿到实例后可以手动触发软电话实例的各种动作,如签入,签出,接听,忽略,挂断等等,注意这个实例不是软电话实例,是链接软电话通讯的实例
|
|
640
|
-
*/
|
|
641
|
-
const nfSoftPhone1 = (new SoftphoneManager()).initSoftphone({ // 👈 当多实例时使用类创建新实例
|
|
642
|
-
el: '#softphone1', // 👈 软电话容器
|
|
643
|
-
selector: '#softpone-context1', // 👈 软电话容器查询上下文,用于多实例隔离软电话dom的查询环境
|
|
644
|
-
ancestorOrigin: 'nf-softphone1', // origin,ancestorOrigin,destinationOrigin具体含义参见下文,如果是单实例,可以写死origin=ai-softphone&destinationOrigin=nf-softphone&ancestorOrigin=nf-softphone
|
|
645
|
-
destinationOrigin: 'nf-softphone1',
|
|
646
|
-
|
|
647
|
-
agentTag: agentTag1, // 👈 坐席唯一标志
|
|
648
|
-
appKey: appKey1, // 👈 企业appKey
|
|
649
|
-
appSecret: appSecret1,
|
|
650
|
-
extStatus: '1', // 👈 初始化是否处于小休状态, 非1 小休 1 在线
|
|
651
|
-
|
|
652
|
-
softphoneConnectCallBack: (data) => {
|
|
653
|
-
console.log('softphone-connect')
|
|
654
|
-
}, // 👈 签入签出回调
|
|
655
|
-
softphoneCallRefreshCallBack: (data) => {console.log('softphone-call-refresh')}, // 👈 来电刷新来电记录列表回调
|
|
656
|
-
softphoneSeatStatusChangeCallBack: (data) => {console.log('softphone-seats-status-change')},// 👈 小休和在线切换回调
|
|
657
|
-
softphoneAcceptCallBack: (data) => {console.log('softphone-accept')},// 👈 接听回调
|
|
658
|
-
softphoneIgnoreCallBack: (data) => {console.log('softphone-ignore')},// 👈 忽略回调
|
|
659
|
-
softphoneHangupCallBack: (data) => {console.log('softphone-hangup')},// 👈 挂断回调
|
|
660
|
-
softphoneSessionStateChangeCallBack: (data) => {console.log('softphone-session-state-change')},// 👈 会话状态改变回调
|
|
661
|
-
softphoneIncomingCallBack: (data) => {console.log('softphone-incoming')},// 👈 来电回调
|
|
662
|
-
softphoneSendDtmfCallBack: (data) => {console.log('softphone-send-dtmf')},// 👈 转人工回调
|
|
663
|
-
softphoneConnectRegisteredCallBack: (data) => {console.log('softphone-connect-registered')},// 👈 动态绑定动作完成回调(在这之后才能执行动作类总线通讯)
|
|
664
|
-
});
|
|
665
|
-
</script>
|
|
666
|
-
</body>
|
|
667
|
-
</html>
|