@robono/calling 0.5.0 → 0.6.0
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/CHANGELOG.md +4 -0
- package/README.md +9 -0
- package/SBOM.spdx.json +70 -22
- package/dist/groups.d.ts +4 -2
- package/dist/groups.js +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +7 -2
- package/dist/recording.d.ts +16 -0
- package/dist/recording.js +116 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
## 0.5.0 — release candidate
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
Adds consent-based managed recording and captions using the current authorized call tracks, immediate withdrawal, retry-safe requests and bounded recording startup/finalization.
|
|
6
|
+
|
|
3
7
|
Add on-demand numeric connection diagnostics for direct and group calls without audio recording or raw network identifiers.
|
|
4
8
|
|
|
5
9
|
## 0.4.0 — 2026-11-04
|
package/README.md
CHANGED
|
@@ -180,3 +180,12 @@ The 0.1.2 engine had polling and retry backoff but did not include this event tr
|
|
|
180
180
|
## Connection details
|
|
181
181
|
|
|
182
182
|
Direct and group clients expose `getDiagnostics()` for local connection measurements without addresses, media or credentials. The supplied call overlays include an optional Connection details panel; custom interfaces can use the same measurements. [Guide, fields and customization](https://robono.com/guides/call-diagnostics.html). Missing measurements remain unknown, and the first sample cannot calculate interval rates.
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
## Recording, captions and local call tools
|
|
186
|
+
|
|
187
|
+
Robono hosts optional call recording and transcription. Enable recording once in the existing Calling settings; every participant must agree before capture, and anyone can stop it. The supplied controls include the consent notice, active-recording status and captions. No customer recording server is required.
|
|
188
|
+
|
|
189
|
+
The supplied adapters also offer camera/microphone/speaker checks and camera background blur. Screen sharing is separate. Phone blur requires rebuilding the native app; browser blur downloads its model only when selected and needs the documented content-security policy. Background processing stays on the device.
|
|
190
|
+
|
|
191
|
+
See the [recording and call tools guide](https://robono.com/guides/call-recording.html) for prices, retention, short-lived downloads, deletion, platform requirements and label customization. Captions arrive in short batches with several seconds of delay and can be inaccurate; incomplete transcripts are marked.
|
package/SBOM.spdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"spdxVersion": "SPDX-2.3",
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
|
-
"name": "@robono/calling@0.
|
|
6
|
-
"documentNamespace": "https://robono.com/sbom/calling/0.
|
|
5
|
+
"name": "@robono/calling@0.6.0",
|
|
6
|
+
"documentNamespace": "https://robono.com/sbom/calling/0.6.0/c5987c1f423c9f088b93fbd9727b38beca106163",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-11-
|
|
8
|
+
"created": "2026-11-05T15:40:42.440Z",
|
|
9
9
|
"creators": [
|
|
10
10
|
"Organization: Robono (support@robono.com)"
|
|
11
11
|
]
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
{
|
|
15
15
|
"name": "@robono/calling",
|
|
16
16
|
"SPDXID": "SPDXRef-Package",
|
|
17
|
-
"versionInfo": "0.
|
|
17
|
+
"versionInfo": "0.6.0",
|
|
18
18
|
"downloadLocation": "NOASSERTION",
|
|
19
19
|
"filesAnalyzed": true,
|
|
20
20
|
"packageVerificationCode": {
|
|
21
|
-
"packageVerificationCodeValue": "
|
|
21
|
+
"packageVerificationCodeValue": "c5987c1f423c9f088b93fbd9727b38beca106163"
|
|
22
22
|
},
|
|
23
23
|
"licenseConcluded": "LicenseRef-RobonoSDK",
|
|
24
24
|
"licenseDeclared": "LicenseRef-RobonoSDK",
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"checksums": [
|
|
37
37
|
{
|
|
38
38
|
"algorithm": "SHA256",
|
|
39
|
-
"checksumValue": "
|
|
39
|
+
"checksumValue": "604c25bc9a6af29a7616e7d0a9a6146187ea34637029393a464c9c305b58acdb"
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
"algorithm": "SHA1",
|
|
43
|
-
"checksumValue": "
|
|
43
|
+
"checksumValue": "d31e5c9fdd55f8af1d09c74220b95e9c40fd336d"
|
|
44
44
|
}
|
|
45
45
|
],
|
|
46
46
|
"licenseConcluded": "LicenseRef-RobonoSDK",
|
|
@@ -74,11 +74,11 @@
|
|
|
74
74
|
"checksums": [
|
|
75
75
|
{
|
|
76
76
|
"algorithm": "SHA256",
|
|
77
|
-
"checksumValue": "
|
|
77
|
+
"checksumValue": "9ce33cd3476398ba270658642687ef92c9acaf163819314435786efbca0777c7"
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
"algorithm": "SHA1",
|
|
81
|
-
"checksumValue": "
|
|
81
|
+
"checksumValue": "1d0174393e6647e3f61e682ce4ff2eeb6cafa59d"
|
|
82
82
|
}
|
|
83
83
|
],
|
|
84
84
|
"licenseConcluded": "LicenseRef-RobonoSDK",
|
|
@@ -131,11 +131,11 @@
|
|
|
131
131
|
"checksums": [
|
|
132
132
|
{
|
|
133
133
|
"algorithm": "SHA256",
|
|
134
|
-
"checksumValue": "
|
|
134
|
+
"checksumValue": "fe6bdfb3689635902da736e75684828b24db440cb1353eecd9e3a80fccdf60dc"
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
137
|
"algorithm": "SHA1",
|
|
138
|
-
"checksumValue": "
|
|
138
|
+
"checksumValue": "f8bb212e7128842941a483cda4af81b56e4d9ed1"
|
|
139
139
|
}
|
|
140
140
|
],
|
|
141
141
|
"licenseConcluded": "LicenseRef-RobonoSDK",
|
|
@@ -150,11 +150,11 @@
|
|
|
150
150
|
"checksums": [
|
|
151
151
|
{
|
|
152
152
|
"algorithm": "SHA256",
|
|
153
|
-
"checksumValue": "
|
|
153
|
+
"checksumValue": "cc1bb25f4b12048b6d0254d108323cd0f759ba0bb9124d0f67699e384ff0f4a8"
|
|
154
154
|
},
|
|
155
155
|
{
|
|
156
156
|
"algorithm": "SHA1",
|
|
157
|
-
"checksumValue": "
|
|
157
|
+
"checksumValue": "2abc7d893f4ed5304b019e4367370abff879ebd0"
|
|
158
158
|
}
|
|
159
159
|
],
|
|
160
160
|
"licenseConcluded": "LicenseRef-RobonoSDK",
|
|
@@ -207,11 +207,11 @@
|
|
|
207
207
|
"checksums": [
|
|
208
208
|
{
|
|
209
209
|
"algorithm": "SHA256",
|
|
210
|
-
"checksumValue": "
|
|
210
|
+
"checksumValue": "60e4d3414d1a05555f709d1241a5d0a70604d4258898b031186f6050736bf103"
|
|
211
211
|
},
|
|
212
212
|
{
|
|
213
213
|
"algorithm": "SHA1",
|
|
214
|
-
"checksumValue": "
|
|
214
|
+
"checksumValue": "87c966dd65e747b929a2f779678a0663b097a0b2"
|
|
215
215
|
}
|
|
216
216
|
],
|
|
217
217
|
"licenseConcluded": "LicenseRef-RobonoSDK",
|
|
@@ -226,11 +226,11 @@
|
|
|
226
226
|
"checksums": [
|
|
227
227
|
{
|
|
228
228
|
"algorithm": "SHA256",
|
|
229
|
-
"checksumValue": "
|
|
229
|
+
"checksumValue": "43fc3a380533b224941f2c3087905380ed5ad4078493dde16d504d11abc499cb"
|
|
230
230
|
},
|
|
231
231
|
{
|
|
232
232
|
"algorithm": "SHA1",
|
|
233
|
-
"checksumValue": "
|
|
233
|
+
"checksumValue": "51a42dd09073ff8d8522b29c67648f5bc94e56bb"
|
|
234
234
|
}
|
|
235
235
|
],
|
|
236
236
|
"licenseConcluded": "LicenseRef-RobonoSDK",
|
|
@@ -297,8 +297,46 @@
|
|
|
297
297
|
"copyrightText": "Copyright (c) 2026 Add to Loop LLC."
|
|
298
298
|
},
|
|
299
299
|
{
|
|
300
|
-
"fileName": "./dist/
|
|
300
|
+
"fileName": "./dist/recording.d.ts",
|
|
301
301
|
"SPDXID": "SPDXRef-File-14",
|
|
302
|
+
"checksums": [
|
|
303
|
+
{
|
|
304
|
+
"algorithm": "SHA256",
|
|
305
|
+
"checksumValue": "46ab2cbe35950e38b5218888a21a5abaa84f309adf31909c00899f7d7fbbc9c6"
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
"algorithm": "SHA1",
|
|
309
|
+
"checksumValue": "5febb29ab24d04d7574ac58088aa59b25a7474d0"
|
|
310
|
+
}
|
|
311
|
+
],
|
|
312
|
+
"licenseConcluded": "LicenseRef-RobonoSDK",
|
|
313
|
+
"licenseInfoInFiles": [
|
|
314
|
+
"LicenseRef-RobonoSDK"
|
|
315
|
+
],
|
|
316
|
+
"copyrightText": "Copyright (c) 2026 Add to Loop LLC."
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
"fileName": "./dist/recording.js",
|
|
320
|
+
"SPDXID": "SPDXRef-File-15",
|
|
321
|
+
"checksums": [
|
|
322
|
+
{
|
|
323
|
+
"algorithm": "SHA256",
|
|
324
|
+
"checksumValue": "878359d91e071cfddd15f26a46fd79efdb0c7a580ef8d7127266807b9d6f6123"
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"algorithm": "SHA1",
|
|
328
|
+
"checksumValue": "dd6b38924bf9d7f3fae259f72cc0bb83207c15ab"
|
|
329
|
+
}
|
|
330
|
+
],
|
|
331
|
+
"licenseConcluded": "LicenseRef-RobonoSDK",
|
|
332
|
+
"licenseInfoInFiles": [
|
|
333
|
+
"LicenseRef-RobonoSDK"
|
|
334
|
+
],
|
|
335
|
+
"copyrightText": "Copyright (c) 2026 Add to Loop LLC."
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"fileName": "./dist/sharing.js",
|
|
339
|
+
"SPDXID": "SPDXRef-File-16",
|
|
302
340
|
"checksums": [
|
|
303
341
|
{
|
|
304
342
|
"algorithm": "SHA256",
|
|
@@ -317,7 +355,7 @@
|
|
|
317
355
|
},
|
|
318
356
|
{
|
|
319
357
|
"fileName": "./dist/telemetry.js",
|
|
320
|
-
"SPDXID": "SPDXRef-File-
|
|
358
|
+
"SPDXID": "SPDXRef-File-17",
|
|
321
359
|
"checksums": [
|
|
322
360
|
{
|
|
323
361
|
"algorithm": "SHA256",
|
|
@@ -336,15 +374,15 @@
|
|
|
336
374
|
},
|
|
337
375
|
{
|
|
338
376
|
"fileName": "./package.json",
|
|
339
|
-
"SPDXID": "SPDXRef-File-
|
|
377
|
+
"SPDXID": "SPDXRef-File-18",
|
|
340
378
|
"checksums": [
|
|
341
379
|
{
|
|
342
380
|
"algorithm": "SHA256",
|
|
343
|
-
"checksumValue": "
|
|
381
|
+
"checksumValue": "7fa0e72cfac51ab3a164ad821bf6dd9b157353b58856687f6fc124c0a3a11e08"
|
|
344
382
|
},
|
|
345
383
|
{
|
|
346
384
|
"algorithm": "SHA1",
|
|
347
|
-
"checksumValue": "
|
|
385
|
+
"checksumValue": "d47d93563ec9e50154c9e4f372e13be27867e2c3"
|
|
348
386
|
}
|
|
349
387
|
],
|
|
350
388
|
"licenseConcluded": "LicenseRef-RobonoSDK",
|
|
@@ -444,6 +482,16 @@
|
|
|
444
482
|
"spdxElementId": "SPDXRef-Package",
|
|
445
483
|
"relationshipType": "CONTAINS",
|
|
446
484
|
"relatedSpdxElement": "SPDXRef-File-16"
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
"spdxElementId": "SPDXRef-Package",
|
|
488
|
+
"relationshipType": "CONTAINS",
|
|
489
|
+
"relatedSpdxElement": "SPDXRef-File-17"
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
"spdxElementId": "SPDXRef-Package",
|
|
493
|
+
"relationshipType": "CONTAINS",
|
|
494
|
+
"relatedSpdxElement": "SPDXRef-File-18"
|
|
447
495
|
}
|
|
448
496
|
],
|
|
449
497
|
"hasExtractedLicensingInfos": [
|
package/dist/groups.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type {CallRecordingController,CallRecordingState} from './recording.js';
|
|
1
2
|
import type {CallDiagnostics,Request,EventStatus} from './index.js';
|
|
2
|
-
export interface GroupCall {screenSharingAvailable?:boolean;sharing?:boolean[];id:string;connectionId:string;group:true;kind:'voice'|'video';version:number;state:'ringing'|'connecting'|'connected'|'ended';reason?:string;createdAt:number;connectedAt:number|null;endedAt:number|null;ringRemainingMs:number;holdExpiresAt:number|null;self:number;controlled:boolean;memberState:'invited'|'joined'|'left'|'declined';localHeld:boolean;members:Array<{id:number;appId:string;userId:string;state:string;held:boolean;connectedAt:number|null}>;peers:Array<{id:string;participant:number;offerer:boolean;remoteDescription:RTCSessionDescriptionInit|null;remoteCandidates:RTCIceCandidateInit[];connected:boolean}>}
|
|
3
|
-
export interface GroupSnapshot {listeningStatus?:EventStatus;screenSharing?:boolean;screenSharePending?:boolean;displayStream?:MediaStream|null;call:GroupCall|null;muted:boolean;cameraEnabled:boolean;localHeld:boolean;localStream:MediaStream|null;remoteStreams:Record<number,MediaStream>;error:string|null;preparing:boolean;connectedAt:number|null}
|
|
3
|
+
export interface GroupCall {recordingAvailable?:boolean;recordingConfirmedAt?:number;recordingState?:import('./recording.js').CallRecordingState['recording'];screenSharingAvailable?:boolean;sharing?:boolean[];id:string;connectionId:string;group:true;kind:'voice'|'video';version:number;state:'ringing'|'connecting'|'connected'|'ended';reason?:string;createdAt:number;connectedAt:number|null;endedAt:number|null;ringRemainingMs:number;holdExpiresAt:number|null;self:number;controlled:boolean;memberState:'invited'|'joined'|'left'|'declined';localHeld:boolean;members:Array<{id:number;appId:string;userId:string;state:string;held:boolean;connectedAt:number|null}>;peers:Array<{id:string;participant:number;offerer:boolean;remoteDescription:RTCSessionDescriptionInit|null;remoteCandidates:RTCIceCandidateInit[];connected:boolean}>}
|
|
4
|
+
export interface GroupSnapshot { recording?:CallRecordingState;listeningStatus?:EventStatus;screenSharing?:boolean;screenSharePending?:boolean;displayStream?:MediaStream|null;call:GroupCall|null;muted:boolean;cameraEnabled:boolean;localHeld:boolean;localStream:MediaStream|null;remoteStreams:Record<number,MediaStream>;error:string|null;preparing:boolean;connectedAt:number|null}
|
|
4
5
|
export class RobonoGroupCallingClient {
|
|
6
|
+
readonly recording:CallRecordingController;
|
|
5
7
|
constructor(options:{videoQuality?:'480p'|'720p';request:Request;onSessionExpired?:(error:Error)=>void;onChange?:(state:GroupSnapshot)=>void;getDisplayMedia?:(context?:{isCurrent:()=>boolean})=>Promise<MediaStream>;getUserMedia?:(context:{kind:'voice'|'video';videoQuality?:'480p'|'720p';isCurrent:()=>boolean})=>Promise<MediaStream>;createPeerConnection?:(config:RTCConfiguration)=>RTCPeerConnection;createRemoteStream?:(track:MediaStreamTrack)=>MediaStream;createId?:()=>string;forceRelay?:boolean;pollMs?:number});
|
|
6
8
|
getDiagnostics():Promise<CallDiagnostics>;
|
|
7
9
|
availability(connectionId:string,kind?:'voice'|'video'):Promise<{available:boolean;maxParticipants:number}>;startScreenSharing():Promise<void>;stopScreenSharing():Promise<void>;state:GroupSnapshot;closed:boolean;listen():Promise<void>;refresh():Promise<boolean>;call(connectionId:string,options?:{kind?:'voice'|'video'}):Promise<void>;answer():Promise<void>;leave():Promise<void>;hold():Promise<void>;resume(prepareAudio?:()=>Promise<void>):Promise<void>;mute(value?:boolean):void;setCamera(value:boolean):void;setForeground(value:boolean):void;setVideoForeground(value:boolean):void;dispose():Promise<void>;
|
package/dist/groups.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {createCallRecording} from './recording.js';
|
|
1
2
|
import {createTransportTelemetry} from './telemetry.js';
|
|
2
3
|
import {createCallDiagnostics} from './diagnostics.js';
|
|
3
4
|
import {mediaProfile,videoConstraints,limitSender} from './media-profile.js';
|
|
@@ -18,10 +19,11 @@ export class RobonoGroupCallingClient {
|
|
|
18
19
|
this.reportTransport=createTransportTelemetry(request);this.peers=new Map();this.pendingRTC=new Set();this.state={call:null,muted:false,cameraEnabled:true,localHeld:false,localStream:null,remoteStreams:{},error:null,preparing:false,connectedAt:null};
|
|
19
20
|
this.getDiagnostics=createCallDiagnostics(()=>({callId:!this.closed&&this.state.call?.controlled&&this.state.call.state!=='ended'?this.state.call.id:null,peers:[...this.peers.values()].map(p=>({pc:p.pc,participant:p.participant}))}));
|
|
20
21
|
this.sharing=createScreenSharing({limitVideo:sender=>limitSender(sender,this.ice?.videoQuality==='480p'?'480p':this.videoQuality),request:input=>this.request('groups/sharing',input),call:()=>this.state.call,peers:()=>[...this.peers.values()].map(p=>p.pc),stream:()=>this.stream,capture:getDisplayMedia,emit:()=>this.emit(),fail:e=>{this.state.error=e.message;this.emit();}});
|
|
22
|
+
this.recording=createCallRecording({request,createPeerConnection,stream:()=>this.stream,videoTrack:()=>this.sharing.track(),iceServers:()=>this.ice?.iceServers??[],onChange:()=>this.emit(),createId});
|
|
21
23
|
this.closed=false;this.epoch=0;this.foreground=true;this.failures=0;this.notBefore=0;this.lastSuccess=Date.now();this.holdRevision=0;
|
|
22
24
|
}
|
|
23
25
|
rtc(promise,phase){return new Promise((resolve,reject)=>{let finished=false;const finish=(fn,value)=>{if(finished)return;finished=true;clearTimeout(timer);this.pendingRTC.delete(cancel);fn(value);};const cancel=()=>finish(reject,Error('call_cancelled'));const timer=setTimeout(()=>finish(reject,Error(phase+'_timeout')),8000);this.pendingRTC.add(cancel);Promise.resolve(promise).then(v=>finish(resolve,v),e=>finish(reject,e));});}
|
|
24
|
-
emit(){if(this.events&&this.state.call&&this.state.call.state!=='ended'&&!this.timer&&!this.polling&&!this.closed)this.timer=setTimeout(()=>this.poll(),3000);this.scheduleRingDeadline();Object.assign(this.state,this.sharing.state());this.onChange({...this.state,remoteStreams:{...this.state.remoteStreams}});}
|
|
26
|
+
emit(){this.recording?.update(this.state.call,this.state.call?.recordingAvailable===true);this.state.recording=this.recording?.snapshot();if(this.events&&this.state.call&&this.state.call.state!=='ended'&&!this.timer&&!this.polling&&!this.closed)this.timer=setTimeout(()=>this.poll(),3000);this.scheduleRingDeadline();Object.assign(this.state,this.sharing.state());this.onChange({...this.state,remoteStreams:{...this.state.remoteStreams}});}
|
|
25
27
|
scheduleRingDeadline(){
|
|
26
28
|
const call=this.state.call;
|
|
27
29
|
if(!call?.controlled||call.state==='ended'||!(call.memberState==='invited'||call.state==='ringing')){clearTimeout(this.ringTimer);this.ringCallId=null;return;}
|
|
@@ -150,6 +152,6 @@ export class RobonoGroupCallingClient {
|
|
|
150
152
|
async hold(){if(!this.state.call||this.state.localHeld)return;this.state.localHeld=true;this.syncTracks();this.emit();await this.sharing.stop().catch(()=>{});try{await this.request('groups/hold',{callId:this.state.call.id,held:true,revision:++this.holdRevision});this.holdDeadline=this.state.call.holdExpiresAt||Date.now()+300000;}catch(e){await this.leave();throw e;}}
|
|
151
153
|
async resume(prepareAudio){if(!this.state.localHeld)return;await prepareAudio?.();await this.request('groups/hold',{callId:this.state.call.id,held:false,revision:++this.holdRevision});this.lastSuccess=Date.now();this.state.localHeld=false;this.syncTracks();this.emit();}
|
|
152
154
|
async leave(){const call=this.state.call;++this.epoch;this.ended??=new Set();if(call)this.ended.add(call.id);while(this.ended.size>256)this.ended.delete(this.ended.values().next().value);this.reset();this.state.call=null;this.state.error=null;this.emit();if(call)await this.request('groups/leave',{callId:call.id});}
|
|
153
|
-
reset(){clearTimeout(this.ringTimer);this.ringCallId=null;this.sharing.reset();++this.epoch;for(const cancel of this.pendingRTC)cancel();this.pendingRTC.clear();this.holdDeadline=0;for(const p of this.peers.values())p.pc.close();this.peers.clear();this.stream?.getTracks().forEach(t=>t.stop());this.stream=null;Object.assign(this.state,{localStream:null,remoteStreams:{},localHeld:false,connectedAt:null});this.holdRevision=0;}
|
|
155
|
+
reset(){this.recording?.update(null,false);clearTimeout(this.ringTimer);this.ringCallId=null;this.sharing.reset();++this.epoch;for(const cancel of this.pendingRTC)cancel();this.pendingRTC.clear();this.holdDeadline=0;for(const p of this.peers.values())p.pc.close();this.peers.clear();this.stream?.getTracks().forEach(t=>t.stop());this.stream=null;Object.assign(this.state,{localStream:null,remoteStreams:{},localHeld:false,connectedAt:null});this.holdRevision=0;}
|
|
154
156
|
async dispose(){this.events?.close();if(this.closed)return;this.closed=true;clearTimeout(this.timer);clearInterval(this.watch);try{await this.leave();}finally{await this.request('close',{}).catch(()=>{});}}
|
|
155
157
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type {CallRecordingController,CallRecordingState} from './recording.js';
|
|
1
2
|
export interface CallMediaDiagnostics {kind:'audio'|'video';sendKbps:number|null;receiveKbps:number|null;receiveLossPercent:number|null;jitterMs:number|null;width:number|null;height:number|null;framesPerSecond:number|null}
|
|
2
3
|
export interface CallPeerDiagnostics {participant:number|null;connectionState:'new'|'connecting'|'connected'|'disconnected'|'failed'|'closed';quality:'unknown'|'good'|'fair'|'poor';roundTripMs:number|null;route:'unknown'|'direct'|'relay';protocol:'udp'|'tcp'|null;media:CallMediaDiagnostics[]}
|
|
3
4
|
/** Local measurements only; contains no addresses, media, SDP or credentials. */
|
|
@@ -6,13 +7,14 @@ export interface CallHistoryEntry { id: string; sequence: number; connectionId:
|
|
|
6
7
|
export interface CallHistoryPage { entries: CallHistoryEntry[]; nextCursor: number; hasMore: boolean }
|
|
7
8
|
export type CallState = 'ringing' | 'connecting' | 'connected' | 'ended';
|
|
8
9
|
export interface Call {
|
|
9
|
-
self?:number;screenSharingAvailable?:boolean;sharing?:boolean[]; id: string; version: number; connectionId: string; kind?: 'voice' | 'video'; state: CallState; role: 'caller' | 'callee'; controlled: boolean; reason?: string; remoteDescription: RTCSessionDescriptionInit | null; remoteCandidates: RTCIceCandidateInit[]; createdAt: number; ringExpiresAt?: number; ringRemainingMs?: number; localHeld?: boolean; remoteHeld?: boolean; holdExpiresAt?: number | null; holdRevision?: number; answeredAt?: number; connectedAt?: number; endedAt?: number }
|
|
10
|
-
export interface Snapshot { listeningStatus?: EventStatus;screenSharing?:boolean;screenSharePending?:boolean;displayStream?:MediaStream|null; voiceAvailable: boolean; videoAvailable?: boolean; cameraEnabled?: boolean; call: Call | null; muted: boolean; localHeld?: boolean; error: string | null; preparing?: boolean; nativePresentation?: { mode?: string; dismissedCallIds?: string[]; speakerEnabled?: boolean; callerNames?: Record<string, string>; appOwnedCallIds?: string[]; appAnsweringCallIds?: string[]; appRingingCallIds: string[]; systemRingingCallIds: string[] } | null }
|
|
10
|
+
self?:number;recordingAvailable?:boolean;recordingConfirmedAt?:number;recordingState?:import('./recording.js').CallRecordingState['recording'];screenSharingAvailable?:boolean;sharing?:boolean[]; id: string; version: number; connectionId: string; kind?: 'voice' | 'video'; state: CallState; role: 'caller' | 'callee'; controlled: boolean; reason?: string; remoteDescription: RTCSessionDescriptionInit | null; remoteCandidates: RTCIceCandidateInit[]; createdAt: number; ringExpiresAt?: number; ringRemainingMs?: number; localHeld?: boolean; remoteHeld?: boolean; holdExpiresAt?: number | null; holdRevision?: number; answeredAt?: number; connectedAt?: number; endedAt?: number }
|
|
11
|
+
export interface Snapshot { recording?:CallRecordingState; listeningStatus?: EventStatus;screenSharing?:boolean;screenSharePending?:boolean;displayStream?:MediaStream|null; voiceAvailable: boolean; videoAvailable?: boolean; cameraEnabled?: boolean; call: Call | null; muted: boolean; localHeld?: boolean; error: string | null; preparing?: boolean; nativePresentation?: { mode?: string; dismissedCallIds?: string[]; speakerEnabled?: boolean; callerNames?: Record<string, string>; appOwnedCallIds?: string[]; appAnsweringCallIds?: string[]; appRingingCallIds: string[]; systemRingingCallIds: string[] } | null }
|
|
11
12
|
export interface EventSubscription { setEnabled(enabled: boolean): void; close(): void }
|
|
12
13
|
export type EventStatus = 'connecting' | 'listening' | 'reconnecting' | 'disconnected' | 'paused' | 'session_expired' | 'access_revoked';
|
|
13
14
|
export interface Request { (operation: string, payload?: Record<string, unknown>): Promise<any>; subscribe?: (callbacks: {onEvent: () => void; onStatus?: (status: EventStatus) => void; onError?: (error: Error & {status?: number}) => void}) => EventSubscription }
|
|
14
15
|
export function createHttpTransport(options: { baseUrl: string; token: string; fetcher?: typeof fetch; events?: boolean; WebSocketImpl?: typeof WebSocket }): Request;
|
|
15
16
|
export class RobonoCallingClient {
|
|
17
|
+
readonly recording:CallRecordingController;
|
|
16
18
|
constructor(options: { videoQuality?: '480p' | '720p'; request: Request; onSessionExpired?: (error: Error) => void; onChange?: (state: Snapshot) => void; onLocalStream?: (stream: MediaStream | null) => void; onRemoteStream?: (stream: MediaStream | null) => void; getDisplayMedia?:(context?:{isCurrent:()=>boolean})=>Promise<MediaStream>;getUserMedia?: (context?: { kind?: 'voice' | 'video'; videoQuality?: '480p'|'720p'; isCurrent: () => boolean }) => Promise<MediaStream>; createPeerConnection?: (config: RTCConfiguration) => RTCPeerConnection; pollMs?: number; forceRelay?: boolean; createId?: () => string; createRemoteStream?: (track: MediaStreamTrack) => MediaStream });
|
|
17
19
|
getDiagnostics():Promise<CallDiagnostics>;
|
|
18
20
|
state: Snapshot;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {createCallRecording} from './recording.js';
|
|
1
2
|
import {subscribeCallingEvents} from './events.js';
|
|
2
3
|
import {createTransportTelemetry} from './telemetry.js';
|
|
3
4
|
import {createCallDiagnostics} from './diagnostics.js';
|
|
@@ -39,7 +40,7 @@ export function createHttpTransport({ baseUrl, token, fetcher = fetch, events =
|
|
|
39
40
|
// Bound the complete operation ourselves, including response-body decoding.
|
|
40
41
|
const deadline = new Promise((_, reject) => { timer = setTimeout(() => {
|
|
41
42
|
controller.abort(); reject(Error('request_timeout'));
|
|
42
|
-
}, 4500); });
|
|
43
|
+
}, ['recording/consent','recording/stop'].includes(operation)?30000:operation==='recording/offer'?20000:operation==='recording/request'?10000:4500); });
|
|
43
44
|
try { return await Promise.race([work, deadline]); } finally { clearTimeout(timer); }
|
|
44
45
|
};
|
|
45
46
|
if(events && WebSocketImpl) request.subscribe = callbacks => subscribeCallingEvents({baseUrl,token,WebSocketImpl},callbacks);
|
|
@@ -65,6 +66,7 @@ export class RobonoCallingClient {
|
|
|
65
66
|
this.sharing=createScreenSharing({limitVideo:sender=>limitSender(sender,this.activeVideoQuality??this.videoQuality),request:input=>this.retry('sharing',input),call:()=>this.state.call,peers:()=>this.pc?[this.pc]:[],stream:()=>this.stream,capture:getDisplayMedia,emit:()=>this.emit(),fail:e=>{this.state.error=e.message;this.emit();}});
|
|
66
67
|
this.reportTransport=createTransportTelemetry(request);
|
|
67
68
|
this.getDiagnostics=createCallDiagnostics(()=>({callId:!this.closed&&this.state.call?.controlled&&this.state.call.state!=='ended'?this.state.call.id:null,peers:this.pc?[{pc:this.pc,participant:null}]:[]}));
|
|
69
|
+
this.recording=createCallRecording({request,createPeerConnection,stream:()=>this.stream,videoTrack:()=>this.sharing.track(),iceServers:()=>this.recordingIce??[],onChange:()=>this.emit(),createId});
|
|
68
70
|
this.localHeld = false; this.holdRevision = 0; this.holdDeadline = 0;
|
|
69
71
|
}
|
|
70
72
|
rtc(promise, phase, timeoutMs = 8000) {
|
|
@@ -77,13 +79,14 @@ export class RobonoCallingClient {
|
|
|
77
79
|
}
|
|
78
80
|
emit() {
|
|
79
81
|
const call = this.state.call;
|
|
82
|
+
this.recording?.update(call,call?.recordingAvailable===true);
|
|
80
83
|
if(this.events && call && call.state!=='ended' && !this.timer && !this.polling && !this.closed)this.timer=setTimeout(()=>this.poll(),3000);
|
|
81
84
|
this.scheduleRingDeadline(call);
|
|
82
85
|
// The local WebRTC transport, not a later server poll, determines when this
|
|
83
86
|
// device can communicate. Keep the server snapshot separate for sequencing.
|
|
84
87
|
const connected = this.mediaConnectedAt != null && this.mediaCallId === call?.id &&
|
|
85
88
|
call.controlled && ['connecting', 'connected'].includes(call.state);
|
|
86
|
-
this.onChange({ ...this.state,...this.sharing.state(), preparing: this.busy, localHeld: this.localHeld,
|
|
89
|
+
this.onChange({ ...this.state,recording:this.recording?.snapshot(),...this.sharing.state(), preparing: this.busy, localHeld: this.localHeld,
|
|
87
90
|
call: connected ? { ...call, state: this.pc?.connectionState === 'disconnected' ? 'connecting' : 'connected', connectedAt: this.mediaConnectedAt } : call });
|
|
88
91
|
}
|
|
89
92
|
scheduleRingDeadline(call) {
|
|
@@ -269,6 +272,7 @@ export class RobonoCallingClient {
|
|
|
269
272
|
}
|
|
270
273
|
}
|
|
271
274
|
resetMedia() {
|
|
275
|
+
this.recording?.update(null,false);
|
|
272
276
|
this.sharing.reset();
|
|
273
277
|
this.epoch++;
|
|
274
278
|
for (const timer of this.mediaTimers) clearTimeout(timer); this.mediaTimers.clear();
|
|
@@ -285,6 +289,7 @@ export class RobonoCallingClient {
|
|
|
285
289
|
const ice = await this.request('ice', { ...(connectionId ? { connectionId } : {}), kind });
|
|
286
290
|
if (this.closed || epoch !== this.epoch) throw Error('call_cancelled');
|
|
287
291
|
if (this.forceRelay && !ice.relayAvailable) throw Error('relay_not_configured');
|
|
292
|
+
this.recordingIce=ice.iceServers;
|
|
288
293
|
this.activeVideoQuality=ice.videoQuality==='480p'?'480p':this.videoQuality;
|
|
289
294
|
const stream = await this.rtc(this.getUserMedia({ kind, videoQuality: ice.videoQuality === '480p' ? '480p' : this.videoQuality, isCurrent: () => !this.closed && epoch === this.epoch }).then(stream => {
|
|
290
295
|
if (this.closed || epoch !== this.epoch) stream.getTracks().forEach(t => t.stop());
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface CallRecordingState {
|
|
2
|
+
available:boolean;
|
|
3
|
+
capabilities?:{available:boolean;audio?:boolean;video?:boolean;captions?:boolean;maxMinutes?:number;retentionDays?:number};
|
|
4
|
+
recording:null|{transcript?:{state:'live'|'complete'|'unavailable';error:string|null;captions:{participant:number;sequence:number;startMs:number;endMs:number;text:string}[]};id:string;revision?:number;createdAt?:number;callId:string;state:'awaiting_consent'|'preparing'|'recording'|'stopping'|'complete'|'failed'|'declined'|'cancelled'|'expired';mode:'audio'|'video';transcribe:boolean;requestedBy:number;consents:{participant:number;accepted:boolean}[];startedAt:number|null;endedAt:number|null;expiresAt:number;reason?:string};
|
|
5
|
+
error:string|null;
|
|
6
|
+
}
|
|
7
|
+
export interface CallRecordingController {
|
|
8
|
+
snapshot():CallRecordingState;
|
|
9
|
+
update(call:{id:string;controlled:boolean;state:string;self:number;recordingConfirmedAt?:number;recordingState?:CallRecordingState['recording']}|null,available:boolean):void;
|
|
10
|
+
start(options?:{mode?:'audio'|'video';transcribe?:boolean;requestId?:string}):Promise<unknown>;
|
|
11
|
+
consent(accept:boolean):Promise<unknown>;
|
|
12
|
+
stop():Promise<void>;
|
|
13
|
+
refresh():Promise<void>;
|
|
14
|
+
dispose():void;
|
|
15
|
+
}
|
|
16
|
+
export function createCallRecording(options:{request:(operation:string,input:Record<string,unknown>)=>Promise<any>;createPeerConnection:(config:{iceServers:RTCIceServer[]})=>RTCPeerConnection;stream:()=>MediaStream|null;videoTrack?:()=>MediaStreamTrack|null|undefined;iceServers?:()=>RTCIceServer[];onChange?:(state:CallRecordingState)=>void;createId?:()=>string;pollMs?:number}):CallRecordingController;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** Publishes the already-authorized call tracks to Robono's recorder.
|
|
2
|
+
* Never opens another microphone or records sound from the phone's speaker.
|
|
3
|
+
*/
|
|
4
|
+
export function createCallRecording({request,createPeerConnection,stream,videoTrack,iceServers=()=>[],onChange=()=>{},createId=()=>crypto.randomUUID(),pollMs=1500}){
|
|
5
|
+
if(!Number.isSafeInteger(pollMs)||pollMs<250||pollMs>5000)throw Error('invalid_recording_poll_interval');
|
|
6
|
+
let call=null,enabled=false,recording=null,error=null,timer=null,busy=false,pc=null,publishing=null,epoch=0,revision=0,disposed=false,lastGood=0,pendingStart=null;
|
|
7
|
+
const withdrawn=new Set();
|
|
8
|
+
let capabilities={available:false},capabilitiesKnown=false,confirmedAt=null;
|
|
9
|
+
const watchdog=setInterval(()=>{if(pc&&Date.now()-lastGood>10000){stopPublisher();error='recording_confirmation_lost';emit();}},1000);watchdog.unref?.();
|
|
10
|
+
const snapshot=()=>({available:enabled&&call?.controlled===true&&call?.state==='connected'&&(capabilities.available===true||!!recording),capabilities,recording,error});
|
|
11
|
+
const emit=()=>onChange(snapshot());
|
|
12
|
+
function stopPublisher(){epoch++;const old=pc;pc=null;publishing=null;if(old){old.onconnectionstatechange=null;old.close();}}
|
|
13
|
+
function setRecord(value){
|
|
14
|
+
if(value!==null&&(!value||value.callId!==call?.id||typeof value.id!=='string'||!['awaiting_consent','preparing','recording','stopping','complete','failed','declined','cancelled','expired'].includes(value.state)||!Array.isArray(value.consents)||value.consents.length>8))throw Error('invalid_recording_state');
|
|
15
|
+
if(recording&&value&&(value.createdAt<recording.createdAt||value.id===recording.id&&value.revision<recording.revision))return;
|
|
16
|
+
recording=value;lastGood=Date.now();error=null;
|
|
17
|
+
if(!value||!['preparing','recording'].includes(value.state))stopPublisher();
|
|
18
|
+
emit();
|
|
19
|
+
}
|
|
20
|
+
function schedule(){clearTimeout(timer);if(!disposed&&enabled&&call?.controlled&&call.state!=='ended'&&(!Object.hasOwn(call,'recordingState')||!capabilitiesKnown)){timer=setTimeout(()=>void refresh(),pollMs);timer.unref?.();}}
|
|
21
|
+
async function gather(connection,generation){
|
|
22
|
+
if(connection.iceGatheringState==='complete')return;
|
|
23
|
+
await new Promise((resolve,reject)=>{
|
|
24
|
+
// Some optional STUN/TURN addresses never finish gathering even after a usable
|
|
25
|
+
// route is available. Send the collected candidates at the deadline; the
|
|
26
|
+
// peer must still establish its authenticated WebRTC connection.
|
|
27
|
+
const deadline=setTimeout(()=>finish(/(?:^|\n)a=candidate:/.test(connection.localDescription?.sdp??'')?undefined:Error('recording_connection_timeout')),8000);
|
|
28
|
+
const interval=setInterval(()=>{if(generation!==epoch||disposed)finish(Error('recording_cancelled'));else if(connection.iceGatheringState==='complete')finish();},50);
|
|
29
|
+
function finish(failure){clearTimeout(deadline);clearInterval(interval);failure?reject(failure):resolve();}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async function publish(){
|
|
33
|
+
const selected=recording,c=call;
|
|
34
|
+
if(Date.now()-lastGood>10000)return;
|
|
35
|
+
if(selected&&withdrawn.has(selected.id))return;
|
|
36
|
+
if(!selected||!c||!['preparing','recording'].includes(selected.state)||selected.consents.some(p=>p.accepted!==true)||!selected.consents.some(p=>p.participant===c.self&&p.accepted))return;
|
|
37
|
+
if(pc){const sender=pc.getSenders().find(s=>s.track?.kind==='video'),track=videoTrack?.()??stream()?.getVideoTracks()[0];if(sender&&track&&sender.track!==track)await sender.replaceTrack(track);return;}
|
|
38
|
+
if(publishing)return publishing;
|
|
39
|
+
const local=stream();if(!local?.getAudioTracks().length)throw Error('recording_call_audio_unavailable');
|
|
40
|
+
const generation=++epoch;const connection=pc=createPeerConnection({iceServers:iceServers()});
|
|
41
|
+
const work=(async()=>{
|
|
42
|
+
for(const track of local.getAudioTracks())connection.addTrack(track,local);
|
|
43
|
+
if(selected.mode==='video'){const track=videoTrack?.()??local.getVideoTracks()[0];if(track)connection.addTrack(track,local);}
|
|
44
|
+
connection.onconnectionstatechange=()=>{if(pc===connection&&connection.connectionState==='failed'){stopPublisher();error='recording_connection_failed';emit();void request('recording/stop',{callId:c.id,id:selected.id}).catch(()=>{});}};
|
|
45
|
+
await connection.setLocalDescription(await connection.createOffer());await gather(connection,generation);
|
|
46
|
+
if(generation!==epoch||call?.id!==c.id)throw Error('recording_cancelled');
|
|
47
|
+
const response=await request('recording/offer',{callId:c.id,id:selected.id,description:{type:connection.localDescription.type,sdp:connection.localDescription.sdp}});
|
|
48
|
+
if(generation!==epoch||call?.id!==c.id)throw Error('recording_cancelled');
|
|
49
|
+
if(response?.description?.type!=='answer'||typeof response.description.sdp!=='string')throw Error('invalid_recording_answer');
|
|
50
|
+
await connection.setRemoteDescription(response.description);
|
|
51
|
+
})().catch(failure=>{if(generation===epoch){stopPublisher();error=failure.message;emit();void request('recording/stop',{callId:c.id,id:selected.id}).catch(()=>{});}throw failure;}).finally(()=>{if(publishing===work)publishing=null;});publishing=work;return work;
|
|
52
|
+
}
|
|
53
|
+
async function refresh(){
|
|
54
|
+
if(busy||disposed||!enabled||!call?.controlled||call.state==='ended')return;
|
|
55
|
+
const id=call.id,version=revision;busy=true;
|
|
56
|
+
try{const result=await request('recording/state',{callId:id});if(disposed||call?.id!==id||version!==revision)return;capabilities=result.capabilities??{available:false};capabilitiesKnown=typeof result.capabilities?.available==='boolean';setRecord(result.recording??null);
|
|
57
|
+
if(recording&&withdrawn.has(recording.id)&&['awaiting_consent','preparing','recording','stopping'].includes(recording.state)){
|
|
58
|
+
const stopped=await request('recording/stop',{callId:id,id:recording.id});
|
|
59
|
+
if(!disposed&&call?.id===id&&version===revision)setRecord(stopped);
|
|
60
|
+
}else await publish();}
|
|
61
|
+
catch(failure){if(!disposed&&call?.id===id&&version===revision){stopPublisher();error=failure.message;emit();}}
|
|
62
|
+
finally{busy=false;schedule();}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
snapshot,
|
|
66
|
+
update(next,available){
|
|
67
|
+
const callChanged=next?.id!==call?.id,changed=callChanged||!!available!==enabled;
|
|
68
|
+
if(changed){revision++;pendingStart=null;}if(callChanged){withdrawn.clear();capabilities={available:false};capabilitiesKnown=false;confirmedAt=null;}call=next;enabled=available===true;
|
|
69
|
+
if(Number.isSafeInteger(next?.recordingConfirmedAt)&&next.recordingConfirmedAt!==confirmedAt){confirmedAt=next.recordingConfirmedAt;lastGood=Date.now();}
|
|
70
|
+
if(!enabled||!call?.controlled||call.state==='ended'||disposed){revision++;clearTimeout(timer);stopPublisher();recording=null;return;}
|
|
71
|
+
if(changed){stopPublisher();recording=null;error=null;void refresh();}
|
|
72
|
+
else if(Object.hasOwn(call,'recordingState')){
|
|
73
|
+
const value=call.recordingState;
|
|
74
|
+
const newer=!recording||value&&(value.id===recording.id?value.revision>recording.revision:value.createdAt>=recording.createdAt);
|
|
75
|
+
if(newer&&JSON.stringify(value)!==JSON.stringify(recording)){try{setRecord(value);void publish().catch(()=>{});}catch{stopPublisher();error='invalid_recording_state';}}
|
|
76
|
+
if(recording&&['preparing','recording'].includes(recording.state)&&stream()?.getAudioTracks().length)void publish().catch(()=>{});
|
|
77
|
+
if(value&&withdrawn.has(value.id)&&recording&&['awaiting_consent','preparing','recording','stopping'].includes(recording.state)&&['awaiting_consent','preparing','recording'].includes(value.state))void refresh();
|
|
78
|
+
}
|
|
79
|
+
// A stalled status response cannot keep capture running without current consent.
|
|
80
|
+
if(pc&&Date.now()-lastGood>10000){stopPublisher();error='recording_confirmation_lost';emit();}
|
|
81
|
+
},
|
|
82
|
+
async start({mode='audio',transcribe=false,requestId}={}){
|
|
83
|
+
if(!enabled||!call?.controlled||call.state!=='connected')throw Error('recording_unavailable');
|
|
84
|
+
if(!['audio','video'].includes(mode)||typeof transcribe!=='boolean')throw Error('invalid_recording_request');
|
|
85
|
+
if(pendingStart&&(pendingStart.mode!==mode||pendingStart.transcribe!==transcribe||requestId&&pendingStart.requestId!==requestId))throw Error('recording_request_pending');
|
|
86
|
+
pendingStart??={mode,transcribe,requestId:requestId??createId()};
|
|
87
|
+
if(pendingStart.promise)return pendingStart.promise;
|
|
88
|
+
const pending=pendingStart,id=call.id,version=++revision;
|
|
89
|
+
const work=(async()=>{
|
|
90
|
+
const result=await request('recording/request',{callId:id,mode,transcribe,requestId:pending.requestId});
|
|
91
|
+
if(call?.id!==id||disposed||version!==revision)throw Error('recording_cancelled');
|
|
92
|
+
setRecord(result);pendingStart=null;schedule();return result;
|
|
93
|
+
})().catch(error=>{if(pendingStart===pending&&error?.status>=400&&error.status<500&&![408,429].includes(error.status))pendingStart=null;throw error;}).finally(()=>{if(pendingStart===pending)pending.promise=null;});
|
|
94
|
+
pending.promise=work;return work;
|
|
95
|
+
},
|
|
96
|
+
async consent(accept){
|
|
97
|
+
if(typeof accept!=='boolean'||!recording||!call)throw Error('recording_not_requested');
|
|
98
|
+
const id=call.id,recordId=recording.id,version=++revision;
|
|
99
|
+
if(accept&&withdrawn.has(recordId))throw Error('recording_consent_withdrawn');
|
|
100
|
+
if(!accept){withdrawn.add(recordId);stopPublisher();}
|
|
101
|
+
try{
|
|
102
|
+
const result=await request('recording/consent',{callId:id,id:recordId,accept});
|
|
103
|
+
if(call?.id!==id||disposed||version!==revision)return;
|
|
104
|
+
setRecord(result);await publish();return result;
|
|
105
|
+
}finally{schedule();}
|
|
106
|
+
},
|
|
107
|
+
async stop(){
|
|
108
|
+
const version=++revision;stopPublisher();if(!recording||!call)return;
|
|
109
|
+
const id=call.id,recordId=recording.id;withdrawn.add(recordId);
|
|
110
|
+
try{const result=await request('recording/stop',{callId:id,id:recordId});if(call?.id===id&&!disposed&&version===revision)setRecord(result);}
|
|
111
|
+
finally{schedule();}
|
|
112
|
+
},
|
|
113
|
+
refresh,
|
|
114
|
+
dispose(){revision++;disposed=true;clearTimeout(timer);clearInterval(watchdog);stopPublisher();call=null;recording=null;},
|
|
115
|
+
};
|
|
116
|
+
}
|
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"provenance": true
|
|
18
18
|
},
|
|
19
19
|
"name": "@robono/calling",
|
|
20
|
-
"version": "0.
|
|
20
|
+
"version": "0.6.0",
|
|
21
21
|
"description": "WebRTC voice, video, group calling and screen sharing within apps and across Robono Bridge networks.",
|
|
22
22
|
"homepage": "https://robono.com/docs#calling",
|
|
23
23
|
"repository": {
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
"main": "./dist/index.js",
|
|
29
29
|
"types": "./dist/index.d.ts",
|
|
30
30
|
"exports": {
|
|
31
|
+
"./recording": {
|
|
32
|
+
"types": "./dist/recording.d.ts",
|
|
33
|
+
"import": "./dist/recording.js"
|
|
34
|
+
},
|
|
31
35
|
".": {
|
|
32
36
|
"types": "./dist/index.d.ts",
|
|
33
37
|
"import": "./dist/index.js"
|