@sentdm/voice-client 0.0.0-stage → 0.1.1

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.
Files changed (99) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +201 -0
  3. package/README.md +283 -2
  4. package/adapter/loader.d.mts +3 -0
  5. package/adapter/loader.d.mts.map +1 -0
  6. package/adapter/loader.d.ts +3 -0
  7. package/adapter/loader.d.ts.map +1 -0
  8. package/adapter/loader.js +6 -0
  9. package/adapter/loader.js.map +1 -0
  10. package/adapter/loader.mjs +3 -0
  11. package/adapter/loader.mjs.map +1 -0
  12. package/adapter/sinch.d.mts +3 -0
  13. package/adapter/sinch.d.mts.map +1 -0
  14. package/adapter/sinch.d.ts +3 -0
  15. package/adapter/sinch.d.ts.map +1 -0
  16. package/adapter/sinch.js +280 -0
  17. package/adapter/sinch.js.map +1 -0
  18. package/adapter/sinch.mjs +276 -0
  19. package/adapter/sinch.mjs.map +1 -0
  20. package/adapter/types.d.mts +78 -0
  21. package/adapter/types.d.mts.map +1 -0
  22. package/adapter/types.d.ts +78 -0
  23. package/adapter/types.d.ts.map +1 -0
  24. package/adapter/types.js +3 -0
  25. package/adapter/types.js.map +1 -0
  26. package/adapter/types.mjs +2 -0
  27. package/adapter/types.mjs.map +1 -0
  28. package/audio.d.mts +42 -0
  29. package/audio.d.mts.map +1 -0
  30. package/audio.d.ts +42 -0
  31. package/audio.d.ts.map +1 -0
  32. package/audio.js +125 -0
  33. package/audio.js.map +1 -0
  34. package/audio.mjs +121 -0
  35. package/audio.mjs.map +1 -0
  36. package/call.d.mts +60 -0
  37. package/call.d.mts.map +1 -0
  38. package/call.d.ts +60 -0
  39. package/call.d.ts.map +1 -0
  40. package/call.js +84 -0
  41. package/call.js.map +1 -0
  42. package/call.mjs +80 -0
  43. package/call.mjs.map +1 -0
  44. package/client.d.mts +111 -0
  45. package/client.d.mts.map +1 -0
  46. package/client.d.ts +111 -0
  47. package/client.d.ts.map +1 -0
  48. package/client.js +365 -0
  49. package/client.js.map +1 -0
  50. package/client.mjs +361 -0
  51. package/client.mjs.map +1 -0
  52. package/errors.d.mts +2 -0
  53. package/errors.d.mts.map +1 -0
  54. package/errors.d.ts +2 -0
  55. package/errors.d.ts.map +1 -0
  56. package/errors.js +16 -0
  57. package/errors.js.map +1 -0
  58. package/errors.mjs +4 -0
  59. package/errors.mjs.map +1 -0
  60. package/index.d.mts +4 -0
  61. package/index.d.mts.map +1 -0
  62. package/index.d.ts +4 -0
  63. package/index.d.ts.map +1 -0
  64. package/index.js +18 -0
  65. package/index.js.map +1 -0
  66. package/index.mjs +4 -0
  67. package/index.mjs.map +1 -0
  68. package/internal/tslib.js +75 -0
  69. package/internal/tslib.mjs +17 -0
  70. package/invite.d.mts +28 -0
  71. package/invite.d.mts.map +1 -0
  72. package/invite.d.ts +28 -0
  73. package/invite.d.ts.map +1 -0
  74. package/invite.js +64 -0
  75. package/invite.js.map +1 -0
  76. package/invite.mjs +60 -0
  77. package/invite.mjs.map +1 -0
  78. package/package.json +41 -3
  79. package/src/adapter/loader.ts +4 -0
  80. package/src/adapter/sinch.ts +333 -0
  81. package/src/adapter/types.ts +63 -0
  82. package/src/audio.ts +174 -0
  83. package/src/call.ts +145 -0
  84. package/src/client.ts +582 -0
  85. package/src/errors.ts +15 -0
  86. package/src/index.ts +14 -0
  87. package/src/invite.ts +75 -0
  88. package/src/sw.js +14 -0
  89. package/src/tsconfig.json +11 -0
  90. package/src/version.ts +1 -0
  91. package/sw.js +14 -0
  92. package/version.d.mts +2 -0
  93. package/version.d.mts.map +1 -0
  94. package/version.d.ts +2 -0
  95. package/version.d.ts.map +1 -0
  96. package/version.js +5 -0
  97. package/version.js.map +1 -0
  98. package/version.mjs +2 -0
  99. package/version.mjs.map +1 -0
package/invite.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { Address, Call } from "./call.js";
2
+ import { type SentVoiceError, TypedEmitter } from '@sentdm/voice-client-core';
3
+ export interface CancelInfo {
4
+ error?: SentVoiceError;
5
+ }
6
+ export interface CallInviteEvents {
7
+ accepted: (call: Call) => void;
8
+ rejected: () => void;
9
+ cancelled: (info: CancelInfo) => void;
10
+ }
11
+ interface InviteProvider {
12
+ answer(): Promise<void>;
13
+ reject(): Promise<void>;
14
+ accepted(): void;
15
+ }
16
+ export declare class CallInvite extends TypedEmitter<CallInviteEvents> {
17
+ #private;
18
+ readonly from: Address;
19
+ readonly to: Address;
20
+ constructor(call: Call, provider: InviteProvider);
21
+ get state(): 'pending' | 'accepted' | 'rejected' | 'cancelled';
22
+ /** Answers the call, asking for microphone permission if it was not granted yet. */
23
+ accept(): Promise<Call>;
24
+ /** Declines the call; does nothing unless it is still pending. */
25
+ reject(): Promise<void>;
26
+ }
27
+ export {};
28
+ //# sourceMappingURL=invite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invite.d.ts","sourceRoot":"","sources":["src/invite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,kBAAe;AAC5C,OAAO,EAAmB,KAAK,cAAc,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAE/F,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAC/B,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;CACvC;AAED,UAAU,cAAc;IACtB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,QAAQ,IAAI,IAAI,CAAC;CAClB;AAID,qBAAa,UAAW,SAAQ,YAAY,CAAC,gBAAgB,CAAC;;IAC5D,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;gBAMT,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,cAAc;IAahD,IAAI,KAAK,IAAI,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,CAE7D;IAED,oFAAoF;IAC9E,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAO7B,kEAAkE;IAC5D,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAmB9B"}
package/invite.js ADDED
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ var _CallInvite_instances, _CallInvite_call, _CallInvite_provider, _CallInvite_state, _CallInvite_accepting, _CallInvite_accept;
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.CallInvite = void 0;
5
+ const tslib_1 = require("./internal/tslib.js");
6
+ const voice_client_core_1 = require("@sentdm/voice-client-core");
7
+ const endedError = () => new voice_client_core_1.CallFailedError({ message: 'The call ended before it was answered.' });
8
+ class CallInvite extends voice_client_core_1.TypedEmitter {
9
+ constructor(call, provider) {
10
+ super();
11
+ _CallInvite_instances.add(this);
12
+ _CallInvite_call.set(this, void 0);
13
+ _CallInvite_provider.set(this, void 0);
14
+ _CallInvite_state.set(this, 'pending');
15
+ _CallInvite_accepting.set(this, void 0);
16
+ this.from = call.from;
17
+ this.to = call.to;
18
+ tslib_1.__classPrivateFieldSet(this, _CallInvite_call, call, "f");
19
+ tslib_1.__classPrivateFieldSet(this, _CallInvite_provider, provider, "f");
20
+ call.on('disconnected', ({ error }) => {
21
+ if (tslib_1.__classPrivateFieldGet(this, _CallInvite_state, "f") !== 'pending')
22
+ return;
23
+ tslib_1.__classPrivateFieldSet(this, _CallInvite_state, 'cancelled', "f");
24
+ this.emit('cancelled', error ? { error } : {});
25
+ });
26
+ }
27
+ get state() {
28
+ return tslib_1.__classPrivateFieldGet(this, _CallInvite_state, "f");
29
+ }
30
+ /** Answers the call, asking for microphone permission if it was not granted yet. */
31
+ async accept() {
32
+ if (tslib_1.__classPrivateFieldGet(this, _CallInvite_state, "f") === 'accepted')
33
+ return tslib_1.__classPrivateFieldGet(this, _CallInvite_call, "f");
34
+ if (tslib_1.__classPrivateFieldGet(this, _CallInvite_state, "f") !== 'pending')
35
+ throw endedError();
36
+ tslib_1.__classPrivateFieldSet(this, _CallInvite_accepting, tslib_1.__classPrivateFieldGet(this, _CallInvite_accepting, "f") ?? tslib_1.__classPrivateFieldGet(this, _CallInvite_instances, "m", _CallInvite_accept).call(this).finally(() => (tslib_1.__classPrivateFieldSet(this, _CallInvite_accepting, undefined, "f"))), "f");
37
+ return tslib_1.__classPrivateFieldGet(this, _CallInvite_accepting, "f");
38
+ }
39
+ /** Declines the call; does nothing unless it is still pending. */
40
+ async reject() {
41
+ if (tslib_1.__classPrivateFieldGet(this, _CallInvite_state, "f") !== 'pending')
42
+ return;
43
+ tslib_1.__classPrivateFieldSet(this, _CallInvite_state, 'rejected', "f");
44
+ this.emit('rejected');
45
+ await tslib_1.__classPrivateFieldGet(this, _CallInvite_provider, "f").reject();
46
+ }
47
+ }
48
+ exports.CallInvite = CallInvite;
49
+ _CallInvite_call = new WeakMap(), _CallInvite_provider = new WeakMap(), _CallInvite_state = new WeakMap(), _CallInvite_accepting = new WeakMap(), _CallInvite_instances = new WeakSet(), _CallInvite_accept = async function _CallInvite_accept() {
50
+ try {
51
+ await tslib_1.__classPrivateFieldGet(this, _CallInvite_provider, "f").answer();
52
+ }
53
+ catch (error) {
54
+ if (tslib_1.__classPrivateFieldGet(this, _CallInvite_state, "f") === 'pending')
55
+ throw error;
56
+ }
57
+ if (tslib_1.__classPrivateFieldGet(this, _CallInvite_state, "f") !== 'pending')
58
+ throw endedError();
59
+ tslib_1.__classPrivateFieldSet(this, _CallInvite_state, 'accepted', "f");
60
+ tslib_1.__classPrivateFieldGet(this, _CallInvite_provider, "f").accepted();
61
+ this.emit('accepted', tslib_1.__classPrivateFieldGet(this, _CallInvite_call, "f"));
62
+ return tslib_1.__classPrivateFieldGet(this, _CallInvite_call, "f");
63
+ };
64
+ //# sourceMappingURL=invite.js.map
package/invite.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invite.js","sourceRoot":"","sources":["src/invite.ts"],"names":[],"mappings":";;;;;AACA,iEAA+F;AAkB/F,MAAM,UAAU,GAAG,GAAG,EAAE,CAAC,IAAI,mCAAe,CAAC,EAAE,OAAO,EAAE,wCAAwC,EAAE,CAAC,CAAC;AAEpG,MAAa,UAAW,SAAQ,gCAA8B;IAQ5D,YAAY,IAAU,EAAE,QAAwB;QAC9C,KAAK,EAAE,CAAC;;QANV,mCAAY;QACZ,uCAA0B;QAC1B,4BAA4D,SAAS,EAAC;QACtE,wCAAsC;QAIpC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;QAClB,+BAAA,IAAI,oBAAS,IAAI,MAAA,CAAC;QAClB,+BAAA,IAAI,wBAAa,QAAQ,MAAA,CAAC;QAC1B,IAAI,CAAC,EAAE,CAAC,cAAc,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE;YACpC,IAAI,+BAAA,IAAI,yBAAO,KAAK,SAAS;gBAAE,OAAO;YACtC,+BAAA,IAAI,qBAAU,WAAW,MAAA,CAAC;YAC1B,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,KAAK;QACP,OAAO,+BAAA,IAAI,yBAAO,CAAC;IACrB,CAAC;IAED,oFAAoF;IACpF,KAAK,CAAC,MAAM;QACV,IAAI,+BAAA,IAAI,yBAAO,KAAK,UAAU;YAAE,OAAO,+BAAA,IAAI,wBAAM,CAAC;QAClD,IAAI,+BAAA,IAAI,yBAAO,KAAK,SAAS;YAAE,MAAM,UAAU,EAAE,CAAC;QAClD,gIAAoB,+BAAA,IAAI,iDAAQ,MAAZ,IAAI,CAAU,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,+BAAA,IAAI,yBAAc,SAAS,MAAA,CAAC,CAAC,MAAA,CAAC;QAChF,OAAO,+BAAA,IAAI,6BAAW,CAAC;IACzB,CAAC;IAED,kEAAkE;IAClE,KAAK,CAAC,MAAM;QACV,IAAI,+BAAA,IAAI,yBAAO,KAAK,SAAS;YAAE,OAAO;QACtC,+BAAA,IAAI,qBAAU,UAAU,MAAA,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACtB,MAAM,+BAAA,IAAI,4BAAU,CAAC,MAAM,EAAE,CAAC;IAChC,CAAC;CAcF;AArDD,gCAqDC;8MAZC,KAAK;IACH,IAAI,CAAC;QACH,MAAM,+BAAA,IAAI,4BAAU,CAAC,MAAM,EAAE,CAAC;IAChC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,+BAAA,IAAI,yBAAO,KAAK,SAAS;YAAE,MAAM,KAAK,CAAC;IAC7C,CAAC;IACD,IAAI,+BAAA,IAAI,yBAAO,KAAK,SAAS;QAAE,MAAM,UAAU,EAAE,CAAC;IAClD,+BAAA,IAAI,qBAAU,UAAU,MAAA,CAAC;IACzB,+BAAA,IAAI,4BAAU,CAAC,QAAQ,EAAE,CAAC;IAC1B,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,+BAAA,IAAI,wBAAM,CAAC,CAAC;IAClC,OAAO,+BAAA,IAAI,wBAAM,CAAC;AACpB,CAAC"}
package/invite.mjs ADDED
@@ -0,0 +1,60 @@
1
+ var _CallInvite_instances, _CallInvite_call, _CallInvite_provider, _CallInvite_state, _CallInvite_accepting, _CallInvite_accept;
2
+ import { __classPrivateFieldGet, __classPrivateFieldSet } from "./internal/tslib.mjs";
3
+ import { CallFailedError, TypedEmitter } from '@sentdm/voice-client-core';
4
+ const endedError = () => new CallFailedError({ message: 'The call ended before it was answered.' });
5
+ export class CallInvite extends TypedEmitter {
6
+ constructor(call, provider) {
7
+ super();
8
+ _CallInvite_instances.add(this);
9
+ _CallInvite_call.set(this, void 0);
10
+ _CallInvite_provider.set(this, void 0);
11
+ _CallInvite_state.set(this, 'pending');
12
+ _CallInvite_accepting.set(this, void 0);
13
+ this.from = call.from;
14
+ this.to = call.to;
15
+ __classPrivateFieldSet(this, _CallInvite_call, call, "f");
16
+ __classPrivateFieldSet(this, _CallInvite_provider, provider, "f");
17
+ call.on('disconnected', ({ error }) => {
18
+ if (__classPrivateFieldGet(this, _CallInvite_state, "f") !== 'pending')
19
+ return;
20
+ __classPrivateFieldSet(this, _CallInvite_state, 'cancelled', "f");
21
+ this.emit('cancelled', error ? { error } : {});
22
+ });
23
+ }
24
+ get state() {
25
+ return __classPrivateFieldGet(this, _CallInvite_state, "f");
26
+ }
27
+ /** Answers the call, asking for microphone permission if it was not granted yet. */
28
+ async accept() {
29
+ if (__classPrivateFieldGet(this, _CallInvite_state, "f") === 'accepted')
30
+ return __classPrivateFieldGet(this, _CallInvite_call, "f");
31
+ if (__classPrivateFieldGet(this, _CallInvite_state, "f") !== 'pending')
32
+ throw endedError();
33
+ __classPrivateFieldSet(this, _CallInvite_accepting, __classPrivateFieldGet(this, _CallInvite_accepting, "f") ?? __classPrivateFieldGet(this, _CallInvite_instances, "m", _CallInvite_accept).call(this).finally(() => (__classPrivateFieldSet(this, _CallInvite_accepting, undefined, "f"))), "f");
34
+ return __classPrivateFieldGet(this, _CallInvite_accepting, "f");
35
+ }
36
+ /** Declines the call; does nothing unless it is still pending. */
37
+ async reject() {
38
+ if (__classPrivateFieldGet(this, _CallInvite_state, "f") !== 'pending')
39
+ return;
40
+ __classPrivateFieldSet(this, _CallInvite_state, 'rejected', "f");
41
+ this.emit('rejected');
42
+ await __classPrivateFieldGet(this, _CallInvite_provider, "f").reject();
43
+ }
44
+ }
45
+ _CallInvite_call = new WeakMap(), _CallInvite_provider = new WeakMap(), _CallInvite_state = new WeakMap(), _CallInvite_accepting = new WeakMap(), _CallInvite_instances = new WeakSet(), _CallInvite_accept = async function _CallInvite_accept() {
46
+ try {
47
+ await __classPrivateFieldGet(this, _CallInvite_provider, "f").answer();
48
+ }
49
+ catch (error) {
50
+ if (__classPrivateFieldGet(this, _CallInvite_state, "f") === 'pending')
51
+ throw error;
52
+ }
53
+ if (__classPrivateFieldGet(this, _CallInvite_state, "f") !== 'pending')
54
+ throw endedError();
55
+ __classPrivateFieldSet(this, _CallInvite_state, 'accepted', "f");
56
+ __classPrivateFieldGet(this, _CallInvite_provider, "f").accepted();
57
+ this.emit('accepted', __classPrivateFieldGet(this, _CallInvite_call, "f"));
58
+ return __classPrivateFieldGet(this, _CallInvite_call, "f");
59
+ };
60
+ //# sourceMappingURL=invite.mjs.map
package/invite.mjs.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invite.mjs","sourceRoot":"","sources":["src/invite.ts"],"names":[],"mappings":";;AACA,OAAO,EAAE,eAAe,EAAuB,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAkB/F,MAAM,UAAU,GAAG,GAAG,EAAE,CAAC,IAAI,eAAe,CAAC,EAAE,OAAO,EAAE,wCAAwC,EAAE,CAAC,CAAC;AAEpG,MAAM,OAAO,UAAW,SAAQ,YAA8B;IAQ5D,YAAY,IAAU,EAAE,QAAwB;QAC9C,KAAK,EAAE,CAAC;;QANV,mCAAY;QACZ,uCAA0B;QAC1B,4BAA4D,SAAS,EAAC;QACtE,wCAAsC;QAIpC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;QAClB,uBAAA,IAAI,oBAAS,IAAI,MAAA,CAAC;QAClB,uBAAA,IAAI,wBAAa,QAAQ,MAAA,CAAC;QAC1B,IAAI,CAAC,EAAE,CAAC,cAAc,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE;YACpC,IAAI,uBAAA,IAAI,yBAAO,KAAK,SAAS;gBAAE,OAAO;YACtC,uBAAA,IAAI,qBAAU,WAAW,MAAA,CAAC;YAC1B,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,KAAK;QACP,OAAO,uBAAA,IAAI,yBAAO,CAAC;IACrB,CAAC;IAED,oFAAoF;IACpF,KAAK,CAAC,MAAM;QACV,IAAI,uBAAA,IAAI,yBAAO,KAAK,UAAU;YAAE,OAAO,uBAAA,IAAI,wBAAM,CAAC;QAClD,IAAI,uBAAA,IAAI,yBAAO,KAAK,SAAS;YAAE,MAAM,UAAU,EAAE,CAAC;QAClD,gHAAoB,uBAAA,IAAI,iDAAQ,MAAZ,IAAI,CAAU,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,uBAAA,IAAI,yBAAc,SAAS,MAAA,CAAC,CAAC,MAAA,CAAC;QAChF,OAAO,uBAAA,IAAI,6BAAW,CAAC;IACzB,CAAC;IAED,kEAAkE;IAClE,KAAK,CAAC,MAAM;QACV,IAAI,uBAAA,IAAI,yBAAO,KAAK,SAAS;YAAE,OAAO;QACtC,uBAAA,IAAI,qBAAU,UAAU,MAAA,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACtB,MAAM,uBAAA,IAAI,4BAAU,CAAC,MAAM,EAAE,CAAC;IAChC,CAAC;CAcF;8MAZC,KAAK;IACH,IAAI,CAAC;QACH,MAAM,uBAAA,IAAI,4BAAU,CAAC,MAAM,EAAE,CAAC;IAChC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,uBAAA,IAAI,yBAAO,KAAK,SAAS;YAAE,MAAM,KAAK,CAAC;IAC7C,CAAC;IACD,IAAI,uBAAA,IAAI,yBAAO,KAAK,SAAS;QAAE,MAAM,UAAU,EAAE,CAAC;IAClD,uBAAA,IAAI,qBAAU,UAAU,MAAA,CAAC;IACzB,uBAAA,IAAI,4BAAU,CAAC,QAAQ,EAAE,CAAC;IAC1B,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,uBAAA,IAAI,wBAAM,CAAC,CAAC;IAClC,OAAO,uBAAA,IAAI,wBAAM,CAAC;AACpB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "@sentdm/voice-client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "Sent Voice Client for the web: calls and incoming calls inside your app",
5
+ "author": "Sent <support@sent.dm>",
6
+ "types": "./index.d.ts",
7
+ "main": "./index.js",
8
+ "type": "commonjs",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/sentdm/sent-dm-voice-typescript.git",
12
+ "directory": "packages/voice-client"
13
+ },
14
+ "license": "Apache-2.0",
15
+ "files": [
16
+ "**/*"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "dependencies": {
22
+ "@sentdm/voice-client-core": "0.1.1",
23
+ "sinch-rtc": "2.49.11"
24
+ },
25
+ "exports": {
26
+ ".": {
27
+ "require": {
28
+ "types": "./index.d.ts",
29
+ "default": "./index.js"
30
+ },
31
+ "types": "./index.d.mts",
32
+ "default": "./index.mjs"
33
+ },
34
+ "./errors": {
35
+ "require": {
36
+ "types": "./errors.d.ts",
37
+ "default": "./errors.js"
38
+ },
39
+ "types": "./errors.d.mts",
40
+ "default": "./errors.mjs"
41
+ },
42
+ "./sw.js": "./sw.js"
43
+ }
6
44
  }
@@ -0,0 +1,4 @@
1
+ import { loadSinchAdapter } from './sinch';
2
+ import type { AdapterFactory } from './types';
3
+
4
+ export const loadAdapter: AdapterFactory = loadSinchAdapter;
@@ -0,0 +1,333 @@
1
+
2
+ import type { Call, CallClient, CallListener, CallQualityWarningEvent, SinchClient } from 'sinch-rtc';
3
+ import type { CallStats } from '../call';
4
+ import {
5
+ CallFailedError,
6
+ CapabilityUnsupportedError,
7
+ MediaPermissionError,
8
+ NetworkError,
9
+ NotRegisteredError,
10
+ SentVoiceError,
11
+ readClaims,
12
+ } from '@sentdm/voice-client-core';
13
+ import type {
14
+ AdapterFactory,
15
+ AdapterOptions,
16
+ CallEvent,
17
+ CallTarget,
18
+ IncomingCall,
19
+ ProviderAdapter,
20
+ } from './types';
21
+
22
+ type Sinch = typeof import('sinch-rtc');
23
+ type StatsCall = Call & { getPeerConnectionStats(): Promise<RTCStatsReport | null> };
24
+
25
+ const environmentHost = 'ocra-euc1.api.sinch.com';
26
+ const applicationPrefix = '//rtc.sinch.com/applications/';
27
+ const answerThrottle = 1_500;
28
+
29
+ const providerError = (error: unknown): SentVoiceError =>
30
+ error instanceof SentVoiceError ? error : (
31
+ new SentVoiceError({
32
+ code: 'UNKNOWN',
33
+ category: 'signaling',
34
+ retriable: false,
35
+ message: 'An unexpected calling error occurred.',
36
+ providerDetail: error,
37
+ })
38
+ );
39
+
40
+ // The Sinch backend refuses a call it cannot set up with `Unable to connect call (<reason>)`, an HTTP
41
+ // failure whose only discriminator is that text. The caller gets a stable code and Sent's wording; the
42
+ // provider's reason is kept as provider detail, for telemetry.
43
+ const setupRefusal = /^Unable to connect call(?: \((.+)\))?/;
44
+ const setupRefusalMessages: Record<string, string> = {
45
+ 'destination user not found': 'The destination is not registered.',
46
+ };
47
+
48
+ const callSetupError = (error: { message: string }): SentVoiceError | undefined => {
49
+ const match = setupRefusal.exec(error.message);
50
+ if (!match) return undefined;
51
+ const reason = match[1];
52
+ const message = reason === undefined ? undefined : setupRefusalMessages[reason];
53
+ return new CallFailedError({
54
+ message: message ?? 'The call could not be connected.',
55
+ providerDetail: error,
56
+ });
57
+ };
58
+
59
+ const rethrow = (error: unknown): never => {
60
+ throw providerError(error);
61
+ };
62
+
63
+ const guard = <T>(action: () => T): T => {
64
+ try {
65
+ return action();
66
+ } catch (error) {
67
+ return rethrow(error);
68
+ }
69
+ };
70
+
71
+ export const loadSinchAdapter: AdapterFactory = async (options) =>
72
+ new SinchAdapter(
73
+ await import('sinch-rtc').catch((error: unknown) => {
74
+ throw new NetworkError({ message: 'The calling provider could not be loaded.', providerDetail: error });
75
+ }),
76
+ options,
77
+ );
78
+
79
+ class SinchAdapter implements ProviderAdapter {
80
+ #sinch: Sinch;
81
+ #options: AdapterOptions;
82
+ #client: SinchClient | undefined;
83
+ #token = '';
84
+ #inputDeviceId: string | undefined;
85
+ #audio: HTMLAudioElement | undefined;
86
+ #calls = new Map<string, Call>();
87
+ #answerRetryAt = new Map<string, number>();
88
+ #incomingListeners: Array<(call: IncomingCall) => void> = [];
89
+ #callEventListeners: Array<(event: CallEvent) => void> = [];
90
+
91
+ constructor(sinch: Sinch, options: AdapterOptions) {
92
+ this.#sinch = sinch;
93
+ this.#options = options;
94
+ }
95
+
96
+ async register(token: string): Promise<void> {
97
+ this.#token = token;
98
+ this.#client ??= await this.#start(token).catch(rethrow);
99
+ }
100
+
101
+ async unregister(): Promise<void> {
102
+ const client = this.#client;
103
+ this.#client = undefined;
104
+ if (client) await this.#stop(client).catch(rethrow);
105
+ }
106
+
107
+ async call(target: CallTarget): Promise<string> {
108
+ const { callClient } = this.#started();
109
+ return this.#placed(
110
+ target.kind === 'user' ? callClient.callUser(target.id) : callClient.callPhoneNumber(target.number),
111
+ );
112
+ }
113
+
114
+ async joinConference(room: string): Promise<string> {
115
+ return this.#placed(this.#started().callClient.callConference(room));
116
+ }
117
+
118
+ async answer(callId: string): Promise<void> {
119
+ const wait = (this.#answerRetryAt.get(callId) ?? 0) - Date.now();
120
+ if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
121
+ const call = this.#calls.get(callId);
122
+ if (!call) return;
123
+ this.#answerRetryAt.set(callId, Date.now() + answerThrottle);
124
+ await call.answer().catch((error: unknown) => {
125
+ throw new MediaPermissionError({ providerDetail: error });
126
+ });
127
+ this.#play(call);
128
+ }
129
+
130
+ async reject(callId: string): Promise<void> {
131
+ return this.hangup(callId);
132
+ }
133
+
134
+ async hangup(callId: string): Promise<void> {
135
+ guard(() => this.#calls.get(callId)?.hangup());
136
+ }
137
+
138
+ mute(callId: string, muted: boolean): void {
139
+ const call = this.#calls.get(callId);
140
+ if (muted) call?.mute();
141
+ else call?.unmute();
142
+ }
143
+
144
+ sendDigits(callId: string, digits: string): void {
145
+ guard(() => this.#calls.get(callId)?.sendDtmf(digits));
146
+ }
147
+
148
+ async getStats(callId: string): Promise<CallStats> {
149
+ const call = this.#calls.get(callId) as StatsCall | undefined;
150
+ const report = await call?.getPeerConnectionStats();
151
+ const stats = { jitter: 0, packetLoss: 0, rtt: 0 };
152
+ report?.forEach((entry) => {
153
+ if (entry.type === 'inbound-rtp' && entry.kind === 'audio') {
154
+ const lost = Math.max(0, entry.packetsLost ?? 0);
155
+ const packets = lost + (entry.packetsReceived ?? 0);
156
+ stats.jitter = (entry.jitter ?? 0) * 1000;
157
+ stats.packetLoss = packets > 0 ? lost / packets : 0;
158
+ } else if (entry.type === 'candidate-pair' && entry.state === 'succeeded') {
159
+ stats.rtt = (entry.currentRoundTripTime ?? 0) * 1000;
160
+ }
161
+ });
162
+ return stats;
163
+ }
164
+
165
+ onIncoming(listener: (call: IncomingCall) => void): void {
166
+ this.#incomingListeners.push(listener);
167
+ }
168
+
169
+ onCallEvent(listener: (event: CallEvent) => void): void {
170
+ this.#callEventListeners.push(listener);
171
+ }
172
+
173
+ async setInputDevice(deviceId: string): Promise<void> {
174
+ this.#inputDeviceId = deviceId;
175
+ if (this.#client?.isStarted()) this.#useInput(this.#client.callClient, deviceId);
176
+ }
177
+
178
+ async setOutputDevice(deviceId: string): Promise<void> {
179
+ const audio = this.#playback();
180
+ if (!('setSinkId' in audio)) {
181
+ throw new CapabilityUnsupportedError({
182
+ message: 'Choosing the audio output device is not supported by this browser.',
183
+ });
184
+ }
185
+ await audio.setSinkId(deviceId).catch(rethrow);
186
+ }
187
+
188
+ async #start(token: string): Promise<SinchClient> {
189
+ const { iss, sub } = readClaims(token) as Record<'iss' | 'sub', string>;
190
+ const client = this.#sinch.Sinch.getSinchClientBuilder()
191
+ .applicationKey(iss.slice(applicationPrefix.length))
192
+ .userId(sub.slice(`${iss}/users/`.length))
193
+ .environmentHost(environmentHost)
194
+ .build();
195
+ const { url, scope } = this.#options.serviceWorker ?? {};
196
+
197
+ try {
198
+ await new Promise<void>((resolve, reject) => {
199
+ client.addListener({
200
+ onCredentialsRequired: (_, registration) => void registration.register(this.#token),
201
+ onClientStarted: ({ callClient }) => {
202
+ callClient.addListener({ onIncomingCall: (_, call) => this.#receive(call) });
203
+ if (this.#inputDeviceId !== undefined) this.#useInput(callClient, this.#inputDeviceId);
204
+ resolve();
205
+ },
206
+ onClientFailed: (_, error) => reject(error),
207
+ });
208
+ client
209
+ .setSupportManagedPush(url, scope === undefined ? undefined : { scope })
210
+ .then(() => client.start(), reject);
211
+ });
212
+ } catch (error) {
213
+ await this.#stop(client).catch(() => {});
214
+ throw error;
215
+ }
216
+ return client;
217
+ }
218
+
219
+ async #stop(client: SinchClient): Promise<void> {
220
+ try {
221
+ await client.disableManagedPushSupport();
222
+ } finally {
223
+ client.terminate();
224
+ }
225
+ }
226
+
227
+ #started(): SinchClient {
228
+ if (!this.#client) throw new NotRegisteredError();
229
+ return this.#client;
230
+ }
231
+
232
+ #useInput(callClient: CallClient, deviceId: string): void {
233
+ callClient.setAudioTrackConstraints({ deviceId });
234
+ }
235
+
236
+ #playback(): HTMLAudioElement {
237
+ return (this.#audio ??= this.#options.audioElement ?? new Audio());
238
+ }
239
+
240
+ async #placed(placing: Promise<Call>): Promise<string> {
241
+ const call = await placing.catch(rethrow);
242
+ if (!call.outgoingStream?.getAudioTracks().length) {
243
+ guard(() => call.hangup());
244
+ throw new MediaPermissionError();
245
+ }
246
+ this.#play(call);
247
+ return this.#track(call);
248
+ }
249
+
250
+ #play(call: Call): void {
251
+ const audio = this.#playback();
252
+ audio.autoplay = true;
253
+ audio.srcObject = call.incomingStream ?? null;
254
+ }
255
+
256
+ #receive(call: Call): void {
257
+ this.#track(call);
258
+ for (const listener of this.#incomingListeners) listener({ callId: call.id, from: call.remoteUserId });
259
+ }
260
+
261
+ #track(call: Call): string {
262
+ const listener: CallListener = {
263
+ onCallProgressing: () => this.#emit({ callId: call.id, type: 'ringing' }),
264
+ onCallAnswered: () => this.#emit({ callId: call.id, type: 'answered' }),
265
+ onCallEstablished: () => this.#emit({ callId: call.id, type: 'connected' }),
266
+ onCallQualityWarningEvent: (_, warning) => {
267
+ const event = this.#warning(call.id, warning);
268
+ if (event) this.#emit(event);
269
+ },
270
+ onCallEnded: () => {
271
+ call.removeListener(listener);
272
+ this.#calls.delete(call.id);
273
+ this.#answerRetryAt.delete(call.id);
274
+ if (this.#audio && this.#audio.srcObject === call.incomingStream) this.#audio.srcObject = null;
275
+ this.#emit(this.#ended(call));
276
+ },
277
+ };
278
+ call.addListener(listener);
279
+ this.#calls.set(call.id, call);
280
+ return call.id;
281
+ }
282
+
283
+ #ended({ id: callId, details: { endCause, error } }: Call): CallEvent {
284
+ const { CallEndCause, ErrorType } = this.#sinch;
285
+ switch (endCause) {
286
+ case CallEndCause.Denied:
287
+ return { callId, type: 'ended', reason: 'busy' };
288
+ case CallEndCause.NoAnswer:
289
+ case CallEndCause.Timeout:
290
+ return { callId, type: 'ended', reason: 'noAnswer' };
291
+ case CallEndCause.Inactive:
292
+ return {
293
+ callId,
294
+ type: 'ended',
295
+ reason: 'failed',
296
+ error: new NetworkError({ providerDetail: error }),
297
+ };
298
+ case CallEndCause.Failure:
299
+ if (!error) return { callId, type: 'ended', reason: 'failed' };
300
+ return {
301
+ callId,
302
+ type: 'ended',
303
+ reason: 'failed',
304
+ error:
305
+ error.domain === ErrorType.Network ?
306
+ new NetworkError({ providerDetail: error })
307
+ : (callSetupError(error) ?? providerError(error)),
308
+ };
309
+ default:
310
+ return { callId, type: 'ended', reason: 'completed' };
311
+ }
312
+ }
313
+
314
+ #warning(callId: string, { name, type }: CallQualityWarningEvent): CallEvent | undefined {
315
+ const cleared = type === this.#sinch.CallQualityWarningEventType.Recover;
316
+ switch (name) {
317
+ case 'missingMediaStream':
318
+ return { callId, type: cleared ? 'reconnected' : 'reconnecting' };
319
+ case 'highInboundJitter':
320
+ return { callId, type: 'qualityWarning', warning: { metric: 'jitter', cleared } };
321
+ case 'highInboundPacketLoss':
322
+ return { callId, type: 'qualityWarning', warning: { metric: 'packetLoss', cleared } };
323
+ case 'highRemoteInboundRtt':
324
+ return { callId, type: 'qualityWarning', warning: { metric: 'rtt', cleared } };
325
+ default:
326
+ return undefined;
327
+ }
328
+ }
329
+
330
+ #emit(event: CallEvent): void {
331
+ for (const listener of this.#callEventListeners) listener(event);
332
+ }
333
+ }
@@ -0,0 +1,63 @@
1
+
2
+ import type { CallStats, QualityWarning } from '../call';
3
+ import { type SentVoiceError } from '@sentdm/voice-client-core';
4
+
5
+ /**
6
+ * The seam between the SDK core and a calling provider. It covers only what the public surface
7
+ * needs and must never be reachable from a public entry point.
8
+ *
9
+ * - Ids are wire ids: the core adds and strips the tenant prefix, so user ids, room names and
10
+ * incoming `from` values are already namespaced.
11
+ * - `register` while registered hands the provider a fresh token.
12
+ * - Every failure is a `SentVoiceError`. A provider error without a mapping becomes one with code
13
+ * `UNKNOWN`, category `signaling` and the raw error kept as provider detail, which only telemetry reads.
14
+ * - Calls are identified by the provider call id. Their events start once the id is known from
15
+ * `call`, `joinConference` or `onIncoming`, and `ended` is the last event of every call,
16
+ * including one ended by `reject` or `hangup`.
17
+ * - `reject` declines an incoming call that has not been answered.
18
+ * - Stats report jitter and rtt in milliseconds and packetLoss as a fraction between 0 and 1.
19
+ */
20
+ export interface ProviderAdapter {
21
+ register(token: string): Promise<void>;
22
+ unregister(): Promise<void>;
23
+ call(target: CallTarget): Promise<string>;
24
+ joinConference(room: string): Promise<string>;
25
+ answer(callId: string): Promise<void>;
26
+ reject(callId: string): Promise<void>;
27
+ hangup(callId: string): Promise<void>;
28
+ mute(callId: string, muted: boolean): void;
29
+ sendDigits(callId: string, digits: string): void;
30
+ getStats(callId: string): Promise<CallStats>;
31
+ onIncoming(listener: (call: IncomingCall) => void): void;
32
+ onCallEvent(listener: (event: CallEvent) => void): void;
33
+ setInputDevice(deviceId: string): Promise<void>;
34
+ setOutputDevice(deviceId: string): Promise<void>;
35
+ }
36
+
37
+ export type AdapterFactory = (options: AdapterOptions) => Promise<ProviderAdapter>;
38
+
39
+ export interface AdapterOptions {
40
+ serviceWorker?: { url?: string | undefined; scope?: string | undefined } | undefined;
41
+ audioElement?: HTMLAudioElement | undefined;
42
+ }
43
+
44
+ export type CallTarget = { kind: 'user'; id: string } | { kind: 'number'; number: string };
45
+
46
+ export interface IncomingCall {
47
+ callId: string;
48
+ from: string;
49
+ }
50
+
51
+ export type CallEvent =
52
+ | { callId: string; type: 'ringing' }
53
+ | { callId: string; type: 'answered' }
54
+ | { callId: string; type: 'connected' }
55
+ | { callId: string; type: 'reconnecting' }
56
+ | { callId: string; type: 'reconnected' }
57
+ | { callId: string; type: 'qualityWarning'; warning: QualityWarning }
58
+ | {
59
+ callId: string;
60
+ type: 'ended';
61
+ reason: 'completed' | 'failed' | 'busy' | 'noAnswer';
62
+ error?: SentVoiceError;
63
+ };