speaker-calibration 2.2.182 → 2.2.184

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 (122) hide show
  1. package/.eslintignore +71 -71
  2. package/.eslintrc.json +40 -40
  3. package/.github/workflows/update-phrases.yml +37 -0
  4. package/.prettierignore +69 -69
  5. package/.prettierrc +14 -14
  6. package/LICENSE +20 -20
  7. package/README.md +133 -133
  8. package/__mocks__/fileMock.js +1 -1
  9. package/__mocks__/styleMock.js +1 -1
  10. package/babel.config.js +3 -3
  11. package/coverage/clover.xml +71 -71
  12. package/coverage/coverage-final.json +224 -224
  13. package/coverage/lcov-report/PythonServerInterface.js.html +265 -265
  14. package/coverage/lcov-report/base.css +354 -354
  15. package/coverage/lcov-report/block-navigation.js +82 -82
  16. package/coverage/lcov-report/index.html +123 -123
  17. package/coverage/lcov-report/prettify.css +101 -101
  18. package/coverage/lcov-report/prettify.js +937 -937
  19. package/coverage/lcov-report/sorter.js +189 -189
  20. package/coverage/lcov-report/src/index.html +121 -121
  21. package/coverage/lcov-report/src/server/PythonServerInterface.js.html +268 -268
  22. package/coverage/lcov-report/src/server/index.html +123 -123
  23. package/coverage/lcov-report/src/tasks/audioCalibrator.js.html +499 -499
  24. package/coverage/lcov-report/src/tasks/audioRecorder.js.html +412 -412
  25. package/coverage/lcov-report/src/tasks/index.html +143 -143
  26. package/coverage/lcov-report/src/tasks/volume/index.html +123 -123
  27. package/coverage/lcov-report/src/tasks/volume/volume.js.html +409 -409
  28. package/coverage/lcov-report/src/utils.js.html +172 -172
  29. package/coverage/lcov.info +91 -91
  30. package/dist/example/NoSleep.min.js +1 -1
  31. package/dist/example/fetch-languages-sheets.js +77 -77
  32. package/dist/example/i18n.js +25828 -25820
  33. package/dist/example/index.html +47 -47
  34. package/dist/example/listener.html +79 -79
  35. package/dist/example/listener.js +149 -149
  36. package/dist/example/server.js +51 -51
  37. package/dist/example/speaker.html +145 -145
  38. package/dist/example/speakerUI.js +273 -273
  39. package/dist/example/styles.css +99 -99
  40. package/dist/main.js +17 -17
  41. package/dist/main.js.LICENSE.txt +118 -0
  42. package/dist/mlsGen.js +6814 -6814
  43. package/dist/mlsGen.wasm +0 -0
  44. package/dist/package-lock.json +1018 -1018
  45. package/dist/package.json +18 -18
  46. package/doc/AudioCalibrator.html +417 -417
  47. package/doc/AudioPeer.html +251 -251
  48. package/doc/AudioRecorder.html +195 -195
  49. package/doc/ImpulseResponse.html +215 -215
  50. package/doc/Listener.html +308 -308
  51. package/doc/MlsGenInterface.html +226 -226
  52. package/doc/MyEventEmitter.html +274 -274
  53. package/doc/PythonServerAPI.html +109 -109
  54. package/doc/Speaker.html +276 -276
  55. package/doc/Takes%20a%20target%20element%20where%20html%20elements%20will%20be%20appended..html +128 -128
  56. package/doc/Takes%20the%20url%20of%20the%20current%20site%0Aand%20a%20target%20element%20where%20html%20elements%20will%20be%20appended..html +138 -138
  57. package/doc/Takes%20the%20url%20of%20the%20current%20site%20and%20a%20target%20element%20where%20html%20elements%20will%20be%20appended..html +137 -137
  58. package/doc/Volume.html +88 -88
  59. package/doc/audioCalibrator.js.html +179 -179
  60. package/doc/audioPeer.js.html +175 -175
  61. package/doc/audioRecorder.js.html +163 -163
  62. package/doc/creates%20a%20new%20AudioRecorder%20instance.%20%0ASets%20up%20the%20audio%20context%20and%20file%20reader..html +114 -114
  63. package/doc/fonts/OpenSans-Bold-webfont.svg +1829 -1829
  64. package/doc/fonts/OpenSans-BoldItalic-webfont.svg +1829 -1829
  65. package/doc/fonts/OpenSans-Italic-webfont.svg +1829 -1829
  66. package/doc/fonts/OpenSans-Light-webfont.svg +1830 -1830
  67. package/doc/fonts/OpenSans-LightItalic-webfont.svg +1834 -1834
  68. package/doc/fonts/OpenSans-Regular-webfont.svg +1830 -1830
  69. package/doc/global.html +308 -308
  70. package/doc/index.html +58 -58
  71. package/doc/listener.js.html +170 -170
  72. package/doc/mlsGen_mlsGenInterface.js.html +117 -117
  73. package/doc/myEventEmitter.js.html +124 -124
  74. package/doc/peer-connection_audioPeer.js.html +188 -188
  75. package/doc/peer-connection_listener.js.html +311 -311
  76. package/doc/peer-connection_speaker.js.html +381 -381
  77. package/doc/scripts/linenumber.js +25 -25
  78. package/doc/scripts/prettify/Apache-License-2.0.txt +202 -202
  79. package/doc/scripts/prettify/lang-css.js +24 -24
  80. package/doc/scripts/prettify/prettify.js +640 -640
  81. package/doc/server_PythonServerAPI.js.html +160 -160
  82. package/doc/speaker.js.html +248 -248
  83. package/doc/styles/jsdoc-default.css +371 -371
  84. package/doc/styles/prettify-jsdoc.css +111 -111
  85. package/doc/styles/prettify-tomorrow.css +163 -163
  86. package/doc/tasks_audioCalibrator.js.html +207 -207
  87. package/doc/tasks_audioRecorder.js.html +190 -190
  88. package/doc/tasks_impulse-response_impulseResponse.js.html +442 -442
  89. package/doc/tasks_impulse-response_mlsGen_mlsGenInterface.js.html +175 -175
  90. package/doc/tasks_volume_volume.js.html +185 -185
  91. package/doc/utils.js.html +105 -105
  92. package/jest.config.js +173 -173
  93. package/makefile +74 -0
  94. package/netlify.toml +26 -26
  95. package/package.json +72 -72
  96. package/src/config/firebase.js +26 -26
  97. package/src/index.html +21 -21
  98. package/src/main.js +23 -23
  99. package/src/myEventEmitter.js +83 -83
  100. package/src/peer-connection/audioPeer.js +178 -178
  101. package/src/peer-connection/listener.js +340 -340
  102. package/src/peer-connection/peerErrors.js +25 -25
  103. package/src/peer-connection/speaker.js +714 -725
  104. package/src/server/PythonServerAPI.js +830 -830
  105. package/src/tasks/audioCalibrator.js +323 -323
  106. package/src/tasks/audioRecorder.js +315 -315
  107. package/src/tasks/combination/combination.js +2955 -2937
  108. package/src/tasks/combination/mlsGen/mlsGen.cpp +98 -98
  109. package/src/tasks/combination/mlsGen/mlsGen.hpp +303 -303
  110. package/src/tasks/combination/mlsGen/mlsGenInterface.js +131 -131
  111. package/src/tasks/combination/mlsGen/mlsGenTest.cpp +180 -180
  112. package/src/tasks/impulse-response/impulseResponse.js +610 -610
  113. package/src/tasks/impulse-response/mlsGen/mlsGen.cpp +98 -98
  114. package/src/tasks/impulse-response/mlsGen/mlsGen.hpp +303 -303
  115. package/src/tasks/impulse-response/mlsGen/mlsGenInterface.js +131 -131
  116. package/src/tasks/impulse-response/mlsGen/mlsGenTest.cpp +180 -180
  117. package/src/tasks/volume/volume.cpp +2 -2
  118. package/src/tasks/volume/volume.hpp +22 -22
  119. package/src/tasks/volume/volume.js +279 -279
  120. package/src/utils.js +205 -205
  121. package/webpack.config.js +37 -37
  122. package/.gitignore +0 -81
@@ -1,725 +1,714 @@
1
- import QRCode from 'qrcode';
2
- import AudioPeer from './audioPeer';
3
- import {
4
- sleep,
5
- formatLineBreak,
6
- createAndShowPopup
7
- } from '../utils';
8
- import {
9
- UnsupportedDeviceError,
10
- MissingSpeakerIdError,
11
- CalibrationTimedOutError,
12
- } from './peerErrors';
13
-
14
- import {phrases} from '../../dist/example/i18n';
15
-
16
- /**
17
- * @class Handles the speaker's side of the connection. Responsible for initiating the connection,
18
- * rendering the QRCode, and answering the call.
19
- * @augments AudioPeer
20
- */
21
- class Speaker extends AudioPeer {
22
- /**
23
- * Takes the url of the current site and a target element where html elements will be appended.
24
- *
25
- * @param params - See type definition for initParameters.
26
- * @param Calibrator - An instance of the AudioCalibrator class, should not use AudioCalibrator directly, instead use an extended class available in /tasks/.
27
- * @param CalibratorInstance
28
- * @example
29
- */
30
- constructor(params, CalibratorInstance) {
31
- super(params);
32
- this.language = params?.language ?? 'en-US';
33
- this.siteUrl += '/listener?';
34
- this.ac = CalibratorInstance;
35
- this.result = null;
36
- this.debug = params?.debug ?? false;
37
- this.isSmartPhone = params?.isSmartPhone ?? false;
38
- this.calibrateSoundHz = params?.calibrateSoundHz ?? 48000;
39
- this.calibrateSoundSamplingDesiredBits = params?.calibrateSoundSamplingDesiredBits ?? 24;
40
- this.instructionDisplayId = params?.instructionDisplayId ?? '';
41
- this.soundSubtitleId = params?.soundSubtitleId ?? '';
42
- this.timeToCalibrateDisplay = params?.timeToCalibrateId ?? '';
43
- this.soundMessageId = params?.soundMessageId ?? '';
44
- this.titleDisplayId = params?.titleDisplayId ?? '';
45
- this.timeToCalibrate = params?.timeToCalibrate ?? 10;
46
- this.isParticipant = params?.isParticipant ?? false;
47
- this.isLoudspeakerCalibration = params?.isLoudspeakerCalibration ?? false;
48
- this.buttonsContainer = params?.buttonsContainer ?? document.createElement('div');
49
-
50
- /* Set up callbacks that handle any events related to our peer object. */
51
- }
52
-
53
- uri = '';
54
- qrImage;
55
- shortURL;
56
-
57
-
58
- initPeer = async () => {
59
- const id = await this.generateTimeBasedPeerID();
60
- this.peer = new Peer(id, {
61
- secure: true,
62
- host: 'easyeyes-peer-server.herokuapp.com',
63
- port: 443,
64
- config: {
65
- iceServers: [
66
- {
67
- urls: "stun:stun.relay.metered.ca:80",
68
- },
69
- {
70
- urls: "turn:global.relay.metered.ca:80",
71
- username: "de884cfc34189cdf1a5dd616",
72
- credential: "IcOpouU9/TYBmpHU",
73
- },
74
- {
75
- urls: "turn:global.relay.metered.ca:80?transport=tcp",
76
- username: "de884cfc34189cdf1a5dd616",
77
- credential: "IcOpouU9/TYBmpHU",
78
- },
79
- {
80
- urls: "turn:global.relay.metered.ca:443",
81
- username: "de884cfc34189cdf1a5dd616",
82
- credential: "IcOpouU9/TYBmpHU",
83
- },
84
- {
85
- urls: "turns:global.relay.metered.ca:443?transport=tcp",
86
- username: "de884cfc34189cdf1a5dd616",
87
- credential: "IcOpouU9/TYBmpHU",
88
- },
89
- ],
90
- },
91
- });
92
- this.peer.on('open', this.#onPeerOpen);
93
- this.peer.on('connection', this.#onPeerConnection);
94
- this.peer.on('close', this.onPeerClose);
95
- this.peer.on('disconnected', this.#onPeerDisconnected);
96
- this.peer.on('error', this.#onPeerError);
97
- }
98
- generateTimeBasedPeerID = async () => {
99
- const now = new Date().getTime();
100
- const randomBuffer = new Uint8Array(10);
101
- crypto.getRandomValues(randomBuffer);
102
- const randomPart = Array.from(randomBuffer)
103
- .map((b) => b.toString(36))
104
- .join("");
105
- const toHash = `${now}-${randomPart}`;
106
- const encoder = new TextEncoder();
107
- const data = encoder.encode(toHash);
108
- const hash = await crypto.subtle.digest("SHA-256", data);
109
- const hashArray = Array.from(new Uint8Array(hash)); // Convert buffer to byte array
110
- const hashString = hashArray
111
- .map((b) => b.toString(16).padStart(2, "0"))
112
- .join("");
113
- const shortHash = hashString.substring(0, 12); // Use more of the hash for a longer ID
114
- // return shortHash; // Consider converting this to Base62
115
- return this.encodeBase62(parseInt(shortHash, 16));
116
- };
117
-
118
- encodeBase62 = (num) => {
119
- const base = 36;
120
- const characters = "0123456789abcdefghijklmnopqrstuvwxyz";
121
- let result = "";
122
- while (num > 0) {
123
- result = characters[num % base] + result;
124
- num = Math.floor(num / base);
125
- }
126
- return result;
127
- };
128
-
129
- /**
130
- * Async factory method that creates the Speaker object, and returns a promise that resolves to the result of the calibration.
131
- *
132
- * @param params - The parameters to be passed to the peer object.
133
- * @param Calibrator - The class that defines the calibration process.
134
- * @param CalibratorInstance
135
- * @param timeOut - The amount of time to wait before timing out the connection (in milliseconds).
136
- * @public
137
- * @example
138
- */
139
- static startCalibration = async (params, CalibratorInstance, timeOut = 180000) => {
140
- window.speaker = new Speaker(params, CalibratorInstance);
141
- const {speaker} = window;
142
- await speaker.initPeer();
143
- // wrap the calibration process in a promise so we can await it
144
- return new Promise((resolve, reject) => {
145
- // when a call is received
146
- speaker.peer.on('call', async call => {
147
- // Answer the call (one way)
148
-
149
- call.answer();
150
- speaker.#removeUIElems();
151
- speaker.#showSpinner();
152
- speaker.ac.createLocalAudio(document.getElementById(speaker.targetElement));
153
- // when we start receiving audio
154
- call.on('stream', async stream => {
155
- window.localStream = stream;
156
- window.localAudio.srcObject = stream;
157
- window.localAudio.autoplay = false;
158
-
159
- // if the sinkSamplingRate is not set sleep
160
- while (!speaker.ac.sampleRatesSet()) {
161
- console.log('SinkSamplingRate is undefined, sleeping');
162
- await sleep(1);
163
- }
164
-
165
- if (params.displayUpdate) {
166
- params.displayUpdate.style.display = '';
167
- }
168
-
169
- // resolve when we have a result
170
- speaker.result = await speaker.ac.startCalibration(
171
- stream,
172
- params.gainValues,
173
- params.ICalib,
174
- params.knownIR,
175
- params.microphoneName,
176
- params.calibrateSoundCheck,
177
- params.isSmartPhone,
178
- params.calibrateSoundBurstDb,
179
- params.calibrateSoundBurstFilteredExtraDb,
180
- params.calibrateSoundBurstLevelReTBool,
181
- params.calibrateSoundBurstUses1000HzGainBool,
182
- params.calibrateSoundBurstRepeats,
183
- params.calibrateSoundBurstSec,
184
- params.calibrateSoundBurstsWarmup,
185
- params.calibrateSoundHz,
186
- params.calibrateSoundIRSec,
187
- params.calibrateSoundIIRSec,
188
- params.calibrateSoundIIRPhase,
189
- params.calibrateSound1000HzPreSec,
190
- params.calibrateSound1000HzSec,
191
- params.calibrateSound1000HzPostSec,
192
- params.calibrateSoundBackgroundSecs,
193
- params.calibrateSoundSmoothOctaves,
194
- params.calibrateSoundSmoothMinBandwidthHz,
195
- params.calibrateSoundPowerBinDesiredSec,
196
- params.calibrateSoundPowerDbSDToleratedDb,
197
- params.calibrateSoundTaperSec,
198
- params.micManufacturer,
199
- params.micSerialNumber,
200
- params.micModelNumber,
201
- params.micModelName,
202
- params.calibrateMicrophonesBool,
203
- params.authorEmails,
204
- params.webAudioDeviceNames,
205
- params.IDsToSaveInSoundProfileLibrary,
206
- params.restartButton,
207
- params.reminder,
208
- params.calibrateSoundLimit
209
- );
210
- speaker.#removeUIElems();
211
- resolve(speaker.result);
212
- });
213
- // if we do not receive a result within the timeout, reject
214
- setTimeout(() => {
215
- reject(
216
- new CalibrationTimedOutError(
217
- `Calibration failed to produce a result after ${
218
- timeOut / 1000
219
- } seconds. Please try again.`
220
- )
221
- );
222
- }, timeOut);
223
- });
224
- });
225
- };
226
-
227
- static testIIR = async (params, CalibratorInstance, IIR, timeOut = 180000) => {
228
- window.speaker = new Speaker(params, CalibratorInstance);
229
- const {speaker} = window;
230
- speaker.initPeer();
231
- // wrap the calibration process in a promise so we can await it
232
- return new Promise((resolve, reject) => {
233
- // when a call is received
234
- speaker.peer.on('call', async call => {
235
- // Answer the call (one way)
236
- call.answer();
237
- speaker.#removeUIElems();
238
- speaker.#showSpinner();
239
- speaker.ac.createLocalAudio(document.getElementById(speaker.targetElement));
240
- // when we start receiving audio
241
- call.on('stream', async stream => {
242
- window.localStream = stream;
243
- window.localAudio.srcObject = stream;
244
- window.localAudio.autoplay = false;
245
-
246
- // if the sinkSamplingRate is not set sleep
247
- while (!speaker.ac.sampleRatesSet()) {
248
- console.log('SinkSamplingRate is undefined, sleeping');
249
- await sleep(1);
250
- }
251
- // resolve when we have a result
252
- speaker.result = await speaker.ac.playMLSwithIIR(stream, IIR);
253
- speaker.#removeUIElems();
254
- resolve(speaker.result);
255
- });
256
- // if we do not receive a result within the timeout, reject
257
- setTimeout(() => {
258
- reject(
259
- new CalibrationTimedOutError(
260
- `Calibration failed to produce a result after ${
261
- timeOut / 1000
262
- } seconds. Please try again.`
263
- )
264
- );
265
- }, timeOut);
266
- });
267
- });
268
- };
269
-
270
- /**
271
- * Called after the peer conncection has been opened.
272
- * Generates a QR code for the connection and displays it.
273
- *
274
- * @private
275
- * @example
276
- */
277
-
278
- #showQRCode = async () => {
279
- // Get query string, the URL parameters to specify a Listener
280
- const queryStringParameters = {
281
- speakerPeerId: this.peer.id,
282
- sp: this.isSmartPhone,
283
- hz: this.calibrateSoundHz,
284
- bits: this.calibrateSoundSamplingDesiredBits,
285
- lang: this.language,
286
- };
287
- const queryString = this.queryStringFromObject(queryStringParameters);
288
- this.uri = this.siteUrl + queryString;
289
- if (this.isSmartPhone) {
290
- // if (true) { // test smartphone QR
291
- // Display QR code for the participant to scan
292
- const qrCanvas = document.createElement('canvas');
293
- qrCanvas.setAttribute('id', 'qrCanvas');
294
- QRCode.toCanvas(qrCanvas, this.uri, error => {
295
- if (error) console.error(error);
296
- });
297
- const explanation = document.createElement("h2");
298
- explanation.id = "skipQRExplanation";
299
- explanation.style = `
300
- user-select: text;
301
- margin-top: 9px;
302
- font-size: 1.1rem;
303
- `;
304
- // Define the URL and options for the request
305
- const url = 'https://api.short.io/links/public';
306
- const options = {
307
- method: 'POST',
308
- headers: {
309
- 'Accept': 'application/json',
310
- 'Content-Type': 'application/json',
311
- 'Authorization': 'pk_fysLKGj3legZz4XZ'
312
- },
313
- body: JSON.stringify({
314
- domain: 'listeners.link', // Ensure this domain is valid for your account
315
- originalURL: this.uri
316
- })
317
- };
318
-
319
- // Make the request using fetch
320
- await fetch(url, options)
321
- .then(response => {
322
- if (!response.ok) {
323
- throw new Error(`HTTP error! Status: ${response.status}`);
324
- }
325
- return response.json(); // Parse the JSON response
326
- })
327
- .then(data => {
328
- explanation.innerHTML =formatLineBreak(
329
- phrases.RC_skipQR_ExplanationWithoutPreferNot[this.language]
330
- .replace("xxx", `<b style="user-select: text">${data.shortURL}</b>`)
331
- .replace("XXX", `<b style="user-select: text">${data.shortURL}</b>`),
332
- phrases.RC_checkInternetConnection[this.language]
333
- );
334
- const checkConnection = document.createElement('a');
335
- checkConnection.id = 'check-connection';
336
- checkConnection.href = '#';
337
- checkConnection.innerHTML = 'check the phone\'s internet connection';
338
- const lang = this.language;
339
- checkConnection.addEventListener('click', function(event,) {
340
- console.log('clicked');
341
- event.preventDefault(); // Prevent the default link action
342
- createAndShowPopup(lang);
343
- });
344
- explanation.querySelector('a#check-connection').replaceWith(checkConnection);
345
- })
346
- .catch(error => {
347
- console.error('Error:', error.message); // Handle errors
348
- });
349
-
350
-
351
-
352
-
353
-
354
- const qrImage = new Image(400, 400);
355
- qrImage.setAttribute('id', 'compatibilityCheckQRImage');
356
- qrImage.style.zIndex = Infinity;
357
- qrImage.style.width = 400;
358
- qrImage.style.height = 400;
359
- qrImage.style.aspectRatio = 1;
360
- qrImage.src = qrCanvas.toDataURL();
361
- qrImage.style.maxHeight = '150px';
362
- qrImage.style.maxWidth = '150px';
363
-
364
- this.qrImage = qrImage;
365
-
366
- const container = document.createElement("div");
367
- container.style.display = "flex";
368
- container.style.justifyContent = "space-between";
369
- container.style.alignItems = "top";
370
- container.id = "skipQRContainer";
371
- container.appendChild(qrImage);
372
- container.appendChild(explanation);
373
- container.appendChild(this.buttonsContainer);
374
- const qrContainer = document.createElement("div");
375
- qrContainer.appendChild(container);
376
-
377
- document.getElementById(this.targetElement).appendChild(qrContainer);
378
- } else {
379
- // show the link to the user
380
- // If specified HTML Id is available, show QR code there
381
- if (document.getElementById(this.targetElement)) {
382
- // const linkTag = document.createElement('a');
383
- // linkTag.setAttribute('href', uri);
384
- // linkTag.innerHTML = 'Click here to start the calibration';
385
- // linkTag.target = '_blank';
386
- // document.getElementById(this.targetElement).appendChild(linkTag);
387
- // document.getElementById(this.targetElement).appendChild(qrCanvas);
388
-
389
- const proceedButton = document.createElement('button');
390
- proceedButton.setAttribute('id', 'calibrationProceedButton');
391
- proceedButton.setAttribute('class', 'btn btn-success');
392
- proceedButton.innerHTML = phrases.T_PROCEED[this.language];
393
- proceedButton.onclick = () => {
394
- // open the link in a new tab
395
- window.open(this.uri, '_blank');
396
- // remove the button
397
- document.getElementById('calibrationProceedButton').remove();
398
- };
399
- document.getElementById(this.targetElement).appendChild(proceedButton);
400
- }
401
- }
402
- // or just print it to console
403
- console.log('TEST: Peer reachable at: ', this.uri);
404
- };
405
-
406
- #showSpinner = () => {
407
- const spinner = document.createElement('div');
408
- spinner.className = 'spinner-border ml-auto';
409
- spinner.role = 'status';
410
- spinner.ariaHidden = 'true';
411
- document.getElementById(this.targetElement).appendChild(spinner);
412
-
413
- // clear instructionDisplay
414
- const soundMessage = document.getElementById(this.soundMessageId);
415
- soundMessage.innerHTML = '';
416
- soundMessage.style.display = 'none';
417
- const instructionDisplay = document.getElementById(this.instructionDisplayId);
418
- const background = document.getElementById('background'); // todo: get background id from params
419
- const subtitle = document.getElementById(this.soundSubtitleId);
420
- if (subtitle) {
421
- subtitle.innerHTML = '';
422
- }
423
- if (instructionDisplay) {
424
- instructionDisplay.innerHTML = '';
425
- instructionDisplay.style.whiteSpace = 'nowrap';
426
- instructionDisplay.style.fontWeight = 'bold';
427
- instructionDisplay.style.width = 'fit-content';
428
- instructionDisplay.innerHTML = phrases.RC_soundRecording[this.language];
429
- let fontSize = 100;
430
- instructionDisplay.style.fontSize = fontSize + 'px';
431
- while (instructionDisplay.scrollWidth > background.scrollWidth * 0.9 && fontSize > 10) {
432
- fontSize--;
433
- instructionDisplay.style.fontSize = fontSize + 'px';
434
- }
435
- // const p = document.createElement('p');
436
- // // font size
437
- // p.style.fontSize = '1.1rem';
438
- // p.style.fontWeight = 'normal';
439
- // p.style.paddingTop = '20px';
440
- // const timeToCalibrateText = phrases.RC_howLongToCalibrate['en-US'];
441
- // p.innerHTML = timeToCalibrateText.replace('111', this.timeToCalibrate);
442
- // instructionDisplay.appendChild(p);
443
- }
444
-
445
- const timeToCalibrateDisplay = document.getElementById(this.timeToCalibrateDisplay);
446
- if (timeToCalibrateDisplay) {
447
- const timeToCalibrateText = phrases.RC_howLongToCalibrate[this.language];
448
- timeToCalibrateDisplay.innerHTML = timeToCalibrateText.replace('111', this.timeToCalibrate);
449
- timeToCalibrateDisplay.style.fontWeight = 'normal';
450
- timeToCalibrateDisplay.style.fontSize = '1rem';
451
- // timeToCalibrateDisplay.style.paddingTop = '20px';
452
- }
453
-
454
- // Update title - titleDisplayId
455
- const titleDisplay = document.getElementById(this.titleDisplayId);
456
- if (titleDisplay) {
457
- // if (this.isParticipant) {
458
- // titleDisplay.innerHTML = titleDisplay.innerHTML.replace('3', '4');
459
- // } else if (this.isSmartPhone) {
460
- // if (this.isLoudspeakerCalibration) {
461
- // titleDisplay.innerHTML = titleDisplay.innerHTML.replace('6', '7');
462
- // } else {
463
- // titleDisplay.innerHTML = titleDisplay.innerHTML.replace('5', '6');
464
- // }
465
- // } else {
466
- // titleDisplay.innerHTML = titleDisplay.innerHTML.replace('5', '6');
467
- // }
468
- if (this.isLoudspeakerCalibration) {
469
- if (this.isParticipant) {
470
- titleDisplay.innerHTML = titleDisplay.innerHTML.replace('3', '4');
471
- } else if (this.isSmartPhone) {
472
- titleDisplay.innerHTML = titleDisplay.innerHTML.replace('6', '7');
473
- } else {
474
- titleDisplay.innerHTML = titleDisplay.innerHTML.replace('5', '6');
475
- }
476
- } else {
477
- if (this.isSmartPhone) {
478
- titleDisplay.innerHTML = titleDisplay.innerHTML.replace('5', '6');
479
- } else {
480
- titleDisplay.innerHTML = titleDisplay.innerHTML.replace('4', '5');
481
- }
482
- }
483
- }
484
- };
485
-
486
- #removeUIElems = () => {
487
- const parent = document.getElementById(this.targetElement);
488
- while (parent.firstChild) {
489
- parent.firstChild.remove();
490
- }
491
- };
492
-
493
- /**
494
- * Called when the peer connection is opened.
495
- * Saves the peer id and calls the QR code generator.
496
- *
497
- * @param peerId - The peer id of the peer connection.
498
- * @param id
499
- * @private
500
- * @example
501
- */
502
- #onPeerOpen = id => {
503
- // Workaround for peer.reconnect deleting previous id
504
- if (id === null) {
505
- console.error('Received null id from peer open');
506
- this.peer.id = this.lastPeerId;
507
- } else {
508
- this.lastPeerId = this.peer.id;
509
- }
510
-
511
- if (id !== this.peer.id) {
512
- console.warn('DEBUG Check you assumption that id === this.peer.id');
513
- }
514
-
515
- this.#showQRCode();
516
- };
517
-
518
- /**
519
- * Called when the peer connection is established.
520
- * Enforces a single connection.
521
- *
522
- * @param connection - The connection object.
523
- * @private
524
- * @example
525
- */
526
- #onPeerConnection = connection => {
527
- // Allow only a single connection
528
- if (this.conn && this.conn.open) {
529
- connection.on('open', () => {
530
- connection.send('Already connected to another client');
531
- setTimeout(() => {
532
- connection.close();
533
- }, 500);
534
- });
535
- return;
536
- }
537
-
538
- this.conn = connection;
539
- console.log('Connected to: ', this.conn.peer);
540
- this.#ready();
541
- };
542
-
543
- /**
544
- * Called when the peer connection is closed.
545
- *
546
- * @private
547
- * @example
548
- */
549
- onPeerClose = () => {
550
- this.conn = null;
551
- console.log('Connection destroyed');
552
- };
553
-
554
- static closeConnection = () => {
555
- this.conn = null;
556
- console.log('Connection destroyed');
557
- };
558
-
559
- /**
560
- * Called when the peer connection is disconnected.
561
- * Attempts to reconnect.
562
- *
563
- * @private
564
- * @example
565
- */
566
- #onPeerDisconnected = () => {
567
- console.log('Connection lost. Please reconnect');
568
-
569
- // Workaround for peer.reconnect deleting previous id
570
- this.peer.id = this.lastPeerId;
571
- // eslint-disable-next-line no-underscore-dangle
572
- this.peer._lastServerId = this.lastPeerId;
573
- this.peer.reconnect();
574
- };
575
-
576
- /**
577
- * Called when the peer connection encounters an error.
578
- *
579
- * @param error
580
- * @private
581
- * @example
582
- */
583
- #onPeerError = error => {
584
- // TODO: check if this function is needed or not
585
- console.error(error);
586
- };
587
-
588
- /**
589
- * Called when data is received from the peer connection.
590
- *
591
- * @param data
592
- * @private
593
- * @example
594
- */
595
- #onIncomingData = data => {
596
- // enforce object type
597
- if (
598
- !Object.prototype.hasOwnProperty.call(data, 'name') ||
599
- !Object.prototype.hasOwnProperty.call(data, 'payload')
600
- ) {
601
- console.error('Received malformed data: ', data);
602
- return;
603
- }
604
-
605
- switch (data.name) {
606
- case 'samplingRate':
607
- this.ac.setSamplingRates(data.payload);
608
- break;
609
- case 'sampleSize':
610
- this.ac.setSampleSize(data.payload);
611
- break;
612
- case 'deviceType':
613
- this.ac.setDeviceType(data.payload);
614
- break;
615
- case 'deviceName':
616
- this.ac.setDeviceName(data.payload);
617
- break;
618
- case 'flags':
619
- //this.ac.setDeviceName(data.payload);
620
- console.log('FLAGS');
621
- console.log(data.payload);
622
- this.ac.setFlags(data.payload);
623
- break;
624
- case 'deviceInfo':
625
- this.ac.setDeviceInfo(data.payload);
626
- console.log('Received device info from listener: ', data.payload);
627
- break;
628
- case UnsupportedDeviceError.name:
629
- case MissingSpeakerIdError.name:
630
- throw data.payload;
631
- break;
632
- default:
633
- break;
634
- }
635
- };
636
-
637
- /**
638
- * Called when the peer connection is #ready.
639
- *
640
- * @private
641
- * @example
642
- */
643
- #ready = () => {
644
- // Perform callback with data
645
- this.conn.on('data', this.#onIncomingData);
646
- this.conn.on('close', () => {
647
- console.log('Connection reset<br>Awaiting connection...');
648
- this.conn = null;
649
- });
650
- };
651
-
652
- /** .
653
- * .
654
- * .
655
- * Debug method for downloading the recorded audio
656
- *
657
- * @public
658
- * @example
659
- */
660
- downloadData = () => {
661
- this.ac.downloadData();
662
- };
663
-
664
- repeatCalibration = async (params, stream, CalibratorInstance) => {
665
- this.ac = CalibratorInstance;
666
- this.#removeUIElems();
667
- this.#showSpinner();
668
-
669
- console.log('This is a repeat');
670
- // wrap the calibration process in a promise so we can await it
671
- return new Promise(async (resolve, reject) => {
672
- const result = await this.ac.startCalibration(
673
- stream,
674
- params.gainValues,
675
- params.ICalib,
676
- params.knownIR,
677
- params.microphoneName,
678
- params.calibrateSoundCheck,
679
- params.isSmartPhone,
680
- params.calibrateSoundBurstDb,
681
- params.calibrateSoundBurstFilteredExtraDb,
682
- params.calibrateSoundBurstLevelReTBool,
683
- params.calibrateSoundBurstUses1000HzGainBool,
684
- params.calibrateSoundBurstRepeats,
685
- params.calibrateSoundBurstSec,
686
- params.calibrateSoundBurstsWarmup,
687
- params.calibrateSoundHz,
688
- params.calibrateSoundIRSec,
689
- params.calibrateSoundIIRSec,
690
- params.params.calibrateSoundIIRPhase,
691
- params.calibrateSound1000HzPreSec,
692
- params.calibrateSound1000HzSec,
693
- params.calibrateSound1000HzPostSec,
694
- params.calibrateSoundBackgroundSecs,
695
- params.calibrateSoundSmoothOctaves,
696
- params.calibrateSoundSmoothMinBandwidthHz,
697
- params.calibrateSoundPowerBinDesiredSec,
698
- params.calibrateSoundPowerDbSDToleratedDb,
699
- params.calibrateSoundTaperSec,
700
- params.micManufacturer,
701
- params.micSerialNumber,
702
- params.micModelNumber,
703
- params.micModelName,
704
- params.calibrateMicrophonesBool,
705
- params.authorEmails,
706
- params.webAudioDeviceNames,
707
- params.IDsToSaveInSoundProfileLibrary,
708
- params.restartButton,
709
- params.reminder,
710
- params.calibrateSoundLimit
711
- );
712
- this.#removeUIElems();
713
- resolve(result);
714
- });
715
- };
716
- }
717
-
718
- /*
719
- Referenced links:
720
- https://stackoverflow.com/questions/28016664/when-you-pass-this-as-an-argument/28016676#28016676
721
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
722
- https://stackoverflow.com/questions/879152/how-do-i-make-javascript-beep [3]
723
- */
724
-
725
- export default Speaker;
1
+ import QRCode from 'qrcode';
2
+ import AudioPeer from './audioPeer';
3
+ import {sleep, formatLineBreak, createAndShowPopup} from '../utils';
4
+ import {
5
+ UnsupportedDeviceError,
6
+ MissingSpeakerIdError,
7
+ CalibrationTimedOutError,
8
+ } from './peerErrors';
9
+
10
+ import {phrases} from '../../dist/example/i18n';
11
+
12
+ /**
13
+ * @class Handles the speaker's side of the connection. Responsible for initiating the connection,
14
+ * rendering the QRCode, and answering the call.
15
+ * @augments AudioPeer
16
+ */
17
+ class Speaker extends AudioPeer {
18
+ /**
19
+ * Takes the url of the current site and a target element where html elements will be appended.
20
+ *
21
+ * @param params - See type definition for initParameters.
22
+ * @param Calibrator - An instance of the AudioCalibrator class, should not use AudioCalibrator directly, instead use an extended class available in /tasks/.
23
+ * @param CalibratorInstance
24
+ * @example
25
+ */
26
+ constructor(params, CalibratorInstance) {
27
+ super(params);
28
+ this.language = params?.language ?? 'en-US';
29
+ this.siteUrl += '/listener?';
30
+ this.ac = CalibratorInstance;
31
+ this.result = null;
32
+ this.debug = params?.debug ?? false;
33
+ this.isSmartPhone = params?.isSmartPhone ?? false;
34
+ this.calibrateSoundHz = params?.calibrateSoundHz ?? 48000;
35
+ this.calibrateSoundSamplingDesiredBits = params?.calibrateSoundSamplingDesiredBits ?? 24;
36
+ this.instructionDisplayId = params?.instructionDisplayId ?? '';
37
+ this.soundSubtitleId = params?.soundSubtitleId ?? '';
38
+ this.timeToCalibrateDisplay = params?.timeToCalibrateId ?? '';
39
+ this.soundMessageId = params?.soundMessageId ?? '';
40
+ this.titleDisplayId = params?.titleDisplayId ?? '';
41
+ this.timeToCalibrate = params?.timeToCalibrate ?? 10;
42
+ this.isParticipant = params?.isParticipant ?? false;
43
+ this.isLoudspeakerCalibration = params?.isLoudspeakerCalibration ?? false;
44
+ this.buttonsContainer = params?.buttonsContainer ?? document.createElement('div');
45
+
46
+ /* Set up callbacks that handle any events related to our peer object. */
47
+ }
48
+
49
+ uri = '';
50
+ qrImage;
51
+ shortURL;
52
+
53
+ initPeer = async () => {
54
+ const id = await this.generateTimeBasedPeerID();
55
+ this.peer = new Peer(id, {
56
+ secure: true,
57
+ host: 'easyeyes-peer-server.herokuapp.com',
58
+ port: 443,
59
+ config: {
60
+ iceServers: [
61
+ {
62
+ urls: 'stun:stun.relay.metered.ca:80',
63
+ },
64
+ {
65
+ urls: 'turn:global.relay.metered.ca:80',
66
+ username: 'de884cfc34189cdf1a5dd616',
67
+ credential: 'IcOpouU9/TYBmpHU',
68
+ },
69
+ {
70
+ urls: 'turn:global.relay.metered.ca:80?transport=tcp',
71
+ username: 'de884cfc34189cdf1a5dd616',
72
+ credential: 'IcOpouU9/TYBmpHU',
73
+ },
74
+ {
75
+ urls: 'turn:global.relay.metered.ca:443',
76
+ username: 'de884cfc34189cdf1a5dd616',
77
+ credential: 'IcOpouU9/TYBmpHU',
78
+ },
79
+ {
80
+ urls: 'turns:global.relay.metered.ca:443?transport=tcp',
81
+ username: 'de884cfc34189cdf1a5dd616',
82
+ credential: 'IcOpouU9/TYBmpHU',
83
+ },
84
+ ],
85
+ },
86
+ });
87
+ this.peer.on('open', this.#onPeerOpen);
88
+ this.peer.on('connection', this.#onPeerConnection);
89
+ this.peer.on('close', this.onPeerClose);
90
+ this.peer.on('disconnected', this.#onPeerDisconnected);
91
+ this.peer.on('error', this.#onPeerError);
92
+ };
93
+ generateTimeBasedPeerID = async () => {
94
+ const now = new Date().getTime();
95
+ const randomBuffer = new Uint8Array(10);
96
+ crypto.getRandomValues(randomBuffer);
97
+ const randomPart = Array.from(randomBuffer)
98
+ .map(b => b.toString(36))
99
+ .join('');
100
+ const toHash = `${now}-${randomPart}`;
101
+ const encoder = new TextEncoder();
102
+ const data = encoder.encode(toHash);
103
+ const hash = await crypto.subtle.digest('SHA-256', data);
104
+ const hashArray = Array.from(new Uint8Array(hash)); // Convert buffer to byte array
105
+ const hashString = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
106
+ const shortHash = hashString.substring(0, 12); // Use more of the hash for a longer ID
107
+ // return shortHash; // Consider converting this to Base62
108
+ return this.encodeBase62(parseInt(shortHash, 16));
109
+ };
110
+
111
+ encodeBase62 = num => {
112
+ const base = 36;
113
+ const characters = '0123456789abcdefghijklmnopqrstuvwxyz';
114
+ let result = '';
115
+ while (num > 0) {
116
+ result = characters[num % base] + result;
117
+ num = Math.floor(num / base);
118
+ }
119
+ return result;
120
+ };
121
+
122
+ /**
123
+ * Async factory method that creates the Speaker object, and returns a promise that resolves to the result of the calibration.
124
+ *
125
+ * @param params - The parameters to be passed to the peer object.
126
+ * @param Calibrator - The class that defines the calibration process.
127
+ * @param CalibratorInstance
128
+ * @param timeOut - The amount of time to wait before timing out the connection (in milliseconds).
129
+ * @public
130
+ * @example
131
+ */
132
+ static startCalibration = async (params, CalibratorInstance, timeOut = 180000) => {
133
+ window.speaker = new Speaker(params, CalibratorInstance);
134
+ const {speaker} = window;
135
+ await speaker.initPeer();
136
+ // wrap the calibration process in a promise so we can await it
137
+ return new Promise((resolve, reject) => {
138
+ // when a call is received
139
+ speaker.peer.on('call', async call => {
140
+ // Answer the call (one way)
141
+
142
+ call.answer();
143
+ speaker.#removeUIElems();
144
+ speaker.#showSpinner();
145
+ speaker.ac.createLocalAudio(document.getElementById(speaker.targetElement));
146
+ // when we start receiving audio
147
+ call.on('stream', async stream => {
148
+ window.localStream = stream;
149
+ window.localAudio.srcObject = stream;
150
+ window.localAudio.autoplay = false;
151
+
152
+ // if the sinkSamplingRate is not set sleep
153
+ while (!speaker.ac.sampleRatesSet()) {
154
+ console.log('SinkSamplingRate is undefined, sleeping');
155
+ await sleep(1);
156
+ }
157
+
158
+ if (params.displayUpdate) {
159
+ params.displayUpdate.style.display = '';
160
+ }
161
+
162
+ // resolve when we have a result
163
+ speaker.result = await speaker.ac.startCalibration(
164
+ stream,
165
+ params.gainValues,
166
+ params.ICalib,
167
+ params.knownIR,
168
+ params.microphoneName,
169
+ params.calibrateSoundCheck,
170
+ params.isSmartPhone,
171
+ params.calibrateSoundBurstDb,
172
+ params.calibrateSoundBurstFilteredExtraDb,
173
+ params.calibrateSoundBurstLevelReTBool,
174
+ params.calibrateSoundBurstUses1000HzGainBool,
175
+ params.calibrateSoundBurstRepeats,
176
+ params.calibrateSoundBurstSec,
177
+ params.calibrateSoundBurstsWarmup,
178
+ params.calibrateSoundHz,
179
+ params.calibrateSoundIRSec,
180
+ params.calibrateSoundIIRSec,
181
+ params.calibrateSoundIIRPhase,
182
+ params.calibrateSound1000HzPreSec,
183
+ params.calibrateSound1000HzSec,
184
+ params.calibrateSound1000HzPostSec,
185
+ params.calibrateSoundBackgroundSecs,
186
+ params.calibrateSoundSmoothOctaves,
187
+ params.calibrateSoundSmoothMinBandwidthHz,
188
+ params.calibrateSoundPowerBinDesiredSec,
189
+ params.calibrateSoundPowerDbSDToleratedDb,
190
+ params.calibrateSoundTaperSec,
191
+ params.micManufacturer,
192
+ params.micSerialNumber,
193
+ params.micModelNumber,
194
+ params.micModelName,
195
+ params.calibrateMicrophonesBool,
196
+ params.authorEmails,
197
+ params.webAudioDeviceNames,
198
+ params.IDsToSaveInSoundProfileLibrary,
199
+ params.restartButton,
200
+ params.reminder,
201
+ params.calibrateSoundLimit
202
+ );
203
+ speaker.#removeUIElems();
204
+ resolve(speaker.result);
205
+ });
206
+ // if we do not receive a result within the timeout, reject
207
+ setTimeout(() => {
208
+ reject(
209
+ new CalibrationTimedOutError(
210
+ `Calibration failed to produce a result after ${
211
+ timeOut / 1000
212
+ } seconds. Please try again.`
213
+ )
214
+ );
215
+ }, timeOut);
216
+ });
217
+ });
218
+ };
219
+
220
+ static testIIR = async (params, CalibratorInstance, IIR, timeOut = 180000) => {
221
+ window.speaker = new Speaker(params, CalibratorInstance);
222
+ const {speaker} = window;
223
+ speaker.initPeer();
224
+ // wrap the calibration process in a promise so we can await it
225
+ return new Promise((resolve, reject) => {
226
+ // when a call is received
227
+ speaker.peer.on('call', async call => {
228
+ // Answer the call (one way)
229
+ call.answer();
230
+ speaker.#removeUIElems();
231
+ speaker.#showSpinner();
232
+ speaker.ac.createLocalAudio(document.getElementById(speaker.targetElement));
233
+ // when we start receiving audio
234
+ call.on('stream', async stream => {
235
+ window.localStream = stream;
236
+ window.localAudio.srcObject = stream;
237
+ window.localAudio.autoplay = false;
238
+
239
+ // if the sinkSamplingRate is not set sleep
240
+ while (!speaker.ac.sampleRatesSet()) {
241
+ console.log('SinkSamplingRate is undefined, sleeping');
242
+ await sleep(1);
243
+ }
244
+ // resolve when we have a result
245
+ speaker.result = await speaker.ac.playMLSwithIIR(stream, IIR);
246
+ speaker.#removeUIElems();
247
+ resolve(speaker.result);
248
+ });
249
+ // if we do not receive a result within the timeout, reject
250
+ setTimeout(() => {
251
+ reject(
252
+ new CalibrationTimedOutError(
253
+ `Calibration failed to produce a result after ${
254
+ timeOut / 1000
255
+ } seconds. Please try again.`
256
+ )
257
+ );
258
+ }, timeOut);
259
+ });
260
+ });
261
+ };
262
+
263
+ /**
264
+ * Called after the peer conncection has been opened.
265
+ * Generates a QR code for the connection and displays it.
266
+ *
267
+ * @private
268
+ * @example
269
+ */
270
+
271
+ #showQRCode = async () => {
272
+ // Get query string, the URL parameters to specify a Listener
273
+ const queryStringParameters = {
274
+ speakerPeerId: this.peer.id,
275
+ sp: this.isSmartPhone,
276
+ hz: this.calibrateSoundHz,
277
+ bits: this.calibrateSoundSamplingDesiredBits,
278
+ lang: this.language,
279
+ };
280
+ const queryString = this.queryStringFromObject(queryStringParameters);
281
+ this.uri = this.siteUrl + queryString;
282
+ if (this.isSmartPhone) {
283
+ // if (true) { // test smartphone QR
284
+ // Display QR code for the participant to scan
285
+ const qrCanvas = document.createElement('canvas');
286
+ qrCanvas.setAttribute('id', 'qrCanvas');
287
+ QRCode.toCanvas(qrCanvas, this.uri, error => {
288
+ if (error) console.error(error);
289
+ });
290
+ const explanation = document.createElement('h2');
291
+ explanation.id = 'skipQRExplanation';
292
+ explanation.style = `
293
+ user-select: text;
294
+ margin-top: 9px;
295
+ font-size: 1.1rem;
296
+ `;
297
+ // Define the URL and options for the request
298
+ const url = 'https://api.short.io/links/public';
299
+ const options = {
300
+ method: 'POST',
301
+ headers: {
302
+ Accept: 'application/json',
303
+ 'Content-Type': 'application/json',
304
+ Authorization: 'pk_fysLKGj3legZz4XZ',
305
+ },
306
+ body: JSON.stringify({
307
+ domain: 'listeners.link', // Ensure this domain is valid for your account
308
+ originalURL: this.uri,
309
+ }),
310
+ };
311
+
312
+ // Make the request using fetch
313
+ await fetch(url, options)
314
+ .then(response => {
315
+ if (!response.ok) {
316
+ throw new Error(`HTTP error! Status: ${response.status}`);
317
+ }
318
+ return response.json(); // Parse the JSON response
319
+ })
320
+ .then(data => {
321
+ explanation.innerHTML = formatLineBreak(
322
+ phrases.RC_skipQR_ExplanationWithoutPreferNot[this.language]
323
+ .replace('xxx', `<b style="user-select: text">${data.shortURL}</b>`)
324
+ .replace('XXX', `<b style="user-select: text">${data.shortURL}</b>`),
325
+ phrases.RC_checkInternetConnection[this.language]
326
+ );
327
+ const checkConnection = document.createElement('a');
328
+ checkConnection.id = 'check-connection';
329
+ checkConnection.href = '#';
330
+ checkConnection.innerHTML = "check the phone's internet connection";
331
+ const lang = this.language;
332
+ checkConnection.addEventListener('click', function (event) {
333
+ console.log('clicked');
334
+ event.preventDefault(); // Prevent the default link action
335
+ createAndShowPopup(lang);
336
+ });
337
+ explanation.querySelector('a#check-connection').replaceWith(checkConnection);
338
+ })
339
+ .catch(error => {
340
+ console.error('Error:', error.message); // Handle errors
341
+ });
342
+
343
+ const qrImage = new Image(400, 400);
344
+ qrImage.setAttribute('id', 'compatibilityCheckQRImage');
345
+ qrImage.style.zIndex = Infinity;
346
+ qrImage.style.width = 400;
347
+ qrImage.style.height = 400;
348
+ qrImage.style.aspectRatio = 1;
349
+ qrImage.src = qrCanvas.toDataURL();
350
+ qrImage.style.maxHeight = '150px';
351
+ qrImage.style.maxWidth = '150px';
352
+
353
+ this.qrImage = qrImage;
354
+
355
+ const container = document.createElement('div');
356
+ container.style.display = 'flex';
357
+ container.style.justifyContent = 'space-between';
358
+ container.style.alignItems = 'top';
359
+ container.id = 'skipQRContainer';
360
+ container.appendChild(qrImage);
361
+ container.appendChild(explanation);
362
+ container.appendChild(this.buttonsContainer);
363
+ const qrContainer = document.createElement('div');
364
+ qrContainer.appendChild(container);
365
+
366
+ document.getElementById(this.targetElement).appendChild(qrContainer);
367
+ } else {
368
+ // show the link to the user
369
+ // If specified HTML Id is available, show QR code there
370
+ if (document.getElementById(this.targetElement)) {
371
+ // const linkTag = document.createElement('a');
372
+ // linkTag.setAttribute('href', uri);
373
+ // linkTag.innerHTML = 'Click here to start the calibration';
374
+ // linkTag.target = '_blank';
375
+ // document.getElementById(this.targetElement).appendChild(linkTag);
376
+ // document.getElementById(this.targetElement).appendChild(qrCanvas);
377
+
378
+ const proceedButton = document.createElement('button');
379
+ proceedButton.setAttribute('id', 'calibrationProceedButton');
380
+ proceedButton.setAttribute('class', 'btn btn-success');
381
+ proceedButton.innerHTML = phrases.T_PROCEED[this.language];
382
+ proceedButton.onclick = () => {
383
+ // open the link in a new tab
384
+ window.open(this.uri, '_blank');
385
+ // remove the button
386
+ document.getElementById('calibrationProceedButton').remove();
387
+ };
388
+ document.getElementById(this.targetElement).appendChild(proceedButton);
389
+ }
390
+ }
391
+ // or just print it to console
392
+ console.log('TEST: Peer reachable at: ', this.uri);
393
+ };
394
+
395
+ #showSpinner = () => {
396
+ const spinner = document.createElement('div');
397
+ spinner.className = 'spinner-border ml-auto';
398
+ spinner.role = 'status';
399
+ spinner.ariaHidden = 'true';
400
+ document.getElementById(this.targetElement).appendChild(spinner);
401
+
402
+ // clear instructionDisplay
403
+ const soundMessage = document.getElementById(this.soundMessageId);
404
+ soundMessage.innerHTML = '';
405
+ soundMessage.style.display = 'none';
406
+ const instructionDisplay = document.getElementById(this.instructionDisplayId);
407
+ const background = document.getElementById('background'); // todo: get background id from params
408
+ const subtitle = document.getElementById(this.soundSubtitleId);
409
+ if (subtitle) {
410
+ subtitle.innerHTML = '';
411
+ }
412
+ if (instructionDisplay) {
413
+ instructionDisplay.innerHTML = '';
414
+ instructionDisplay.style.whiteSpace = 'nowrap';
415
+ instructionDisplay.style.fontWeight = 'bold';
416
+ instructionDisplay.style.width = 'fit-content';
417
+ instructionDisplay.innerHTML = phrases.RC_soundRecording[this.language];
418
+ let fontSize = 100;
419
+ instructionDisplay.style.fontSize = fontSize + 'px';
420
+ while (instructionDisplay.scrollWidth > background.scrollWidth * 0.9 && fontSize > 10) {
421
+ fontSize--;
422
+ instructionDisplay.style.fontSize = fontSize + 'px';
423
+ }
424
+ // const p = document.createElement('p');
425
+ // // font size
426
+ // p.style.fontSize = '1.1rem';
427
+ // p.style.fontWeight = 'normal';
428
+ // p.style.paddingTop = '20px';
429
+ // const timeToCalibrateText = phrases.RC_howLongToCalibrate['en-US'];
430
+ // p.innerHTML = timeToCalibrateText.replace('111', this.timeToCalibrate);
431
+ // instructionDisplay.appendChild(p);
432
+ }
433
+
434
+ const timeToCalibrateDisplay = document.getElementById(this.timeToCalibrateDisplay);
435
+ if (timeToCalibrateDisplay) {
436
+ const timeToCalibrateText = phrases.RC_howLongToCalibrate[this.language];
437
+ timeToCalibrateDisplay.innerHTML = timeToCalibrateText.replace('111', this.timeToCalibrate);
438
+ timeToCalibrateDisplay.style.fontWeight = 'normal';
439
+ timeToCalibrateDisplay.style.fontSize = '1rem';
440
+ // timeToCalibrateDisplay.style.paddingTop = '20px';
441
+ }
442
+
443
+ // Update title - titleDisplayId
444
+ const titleDisplay = document.getElementById(this.titleDisplayId);
445
+ if (titleDisplay) {
446
+ // if (this.isParticipant) {
447
+ // titleDisplay.innerHTML = titleDisplay.innerHTML.replace('3', '4');
448
+ // } else if (this.isSmartPhone) {
449
+ // if (this.isLoudspeakerCalibration) {
450
+ // titleDisplay.innerHTML = titleDisplay.innerHTML.replace('6', '7');
451
+ // } else {
452
+ // titleDisplay.innerHTML = titleDisplay.innerHTML.replace('5', '6');
453
+ // }
454
+ // } else {
455
+ // titleDisplay.innerHTML = titleDisplay.innerHTML.replace('5', '6');
456
+ // }
457
+ if (this.isLoudspeakerCalibration) {
458
+ if (this.isParticipant) {
459
+ titleDisplay.innerHTML = titleDisplay.innerHTML.replace('3', '4');
460
+ } else if (this.isSmartPhone) {
461
+ titleDisplay.innerHTML = titleDisplay.innerHTML.replace('6', '7');
462
+ } else {
463
+ titleDisplay.innerHTML = titleDisplay.innerHTML.replace('5', '6');
464
+ }
465
+ } else {
466
+ if (this.isSmartPhone) {
467
+ titleDisplay.innerHTML = titleDisplay.innerHTML.replace('5', '6');
468
+ } else {
469
+ titleDisplay.innerHTML = titleDisplay.innerHTML.replace('4', '5');
470
+ }
471
+ }
472
+ }
473
+ };
474
+
475
+ #removeUIElems = () => {
476
+ const parent = document.getElementById(this.targetElement);
477
+ while (parent.firstChild) {
478
+ parent.firstChild.remove();
479
+ }
480
+ };
481
+
482
+ /**
483
+ * Called when the peer connection is opened.
484
+ * Saves the peer id and calls the QR code generator.
485
+ *
486
+ * @param peerId - The peer id of the peer connection.
487
+ * @param id
488
+ * @private
489
+ * @example
490
+ */
491
+ #onPeerOpen = id => {
492
+ // Workaround for peer.reconnect deleting previous id
493
+ if (id === null) {
494
+ console.error('Received null id from peer open');
495
+ this.peer.id = this.lastPeerId;
496
+ } else {
497
+ this.lastPeerId = this.peer.id;
498
+ }
499
+
500
+ if (id !== this.peer.id) {
501
+ console.warn('DEBUG Check you assumption that id === this.peer.id');
502
+ }
503
+
504
+ this.#showQRCode();
505
+ };
506
+
507
+ /**
508
+ * Called when the peer connection is established.
509
+ * Enforces a single connection.
510
+ *
511
+ * @param connection - The connection object.
512
+ * @private
513
+ * @example
514
+ */
515
+ #onPeerConnection = connection => {
516
+ // Allow only a single connection
517
+ if (this.conn && this.conn.open) {
518
+ connection.on('open', () => {
519
+ connection.send('Already connected to another client');
520
+ setTimeout(() => {
521
+ connection.close();
522
+ }, 500);
523
+ });
524
+ return;
525
+ }
526
+
527
+ this.conn = connection;
528
+ console.log('Connected to: ', this.conn.peer);
529
+ this.#ready();
530
+ };
531
+
532
+ /**
533
+ * Called when the peer connection is closed.
534
+ *
535
+ * @private
536
+ * @example
537
+ */
538
+ onPeerClose = () => {
539
+ this.conn = null;
540
+ console.log('Connection destroyed');
541
+ };
542
+
543
+ static closeConnection = () => {
544
+ this.conn = null;
545
+ console.log('Connection destroyed');
546
+ };
547
+
548
+ /**
549
+ * Called when the peer connection is disconnected.
550
+ * Attempts to reconnect.
551
+ *
552
+ * @private
553
+ * @example
554
+ */
555
+ #onPeerDisconnected = () => {
556
+ console.log('Connection lost. Please reconnect');
557
+
558
+ // Workaround for peer.reconnect deleting previous id
559
+ this.peer.id = this.lastPeerId;
560
+ // eslint-disable-next-line no-underscore-dangle
561
+ this.peer._lastServerId = this.lastPeerId;
562
+ this.peer.reconnect();
563
+ };
564
+
565
+ /**
566
+ * Called when the peer connection encounters an error.
567
+ *
568
+ * @param error
569
+ * @private
570
+ * @example
571
+ */
572
+ #onPeerError = error => {
573
+ // TODO: check if this function is needed or not
574
+ console.error(error);
575
+ };
576
+
577
+ /**
578
+ * Called when data is received from the peer connection.
579
+ *
580
+ * @param data
581
+ * @private
582
+ * @example
583
+ */
584
+ #onIncomingData = data => {
585
+ // enforce object type
586
+ if (
587
+ !Object.prototype.hasOwnProperty.call(data, 'name') ||
588
+ !Object.prototype.hasOwnProperty.call(data, 'payload')
589
+ ) {
590
+ console.error('Received malformed data: ', data);
591
+ return;
592
+ }
593
+
594
+ switch (data.name) {
595
+ case 'samplingRate':
596
+ this.ac.setSamplingRates(data.payload);
597
+ break;
598
+ case 'sampleSize':
599
+ this.ac.setSampleSize(data.payload);
600
+ break;
601
+ case 'deviceType':
602
+ this.ac.setDeviceType(data.payload);
603
+ break;
604
+ case 'deviceName':
605
+ this.ac.setDeviceName(data.payload);
606
+ break;
607
+ case 'flags':
608
+ //this.ac.setDeviceName(data.payload);
609
+ console.log('FLAGS');
610
+ console.log(data.payload);
611
+ this.ac.setFlags(data.payload);
612
+ break;
613
+ case 'deviceInfo':
614
+ this.ac.setDeviceInfo(data.payload);
615
+ console.log('Received device info from listener: ', data.payload);
616
+ break;
617
+ case UnsupportedDeviceError.name:
618
+ case MissingSpeakerIdError.name:
619
+ throw data.payload;
620
+ break;
621
+ default:
622
+ break;
623
+ }
624
+ };
625
+
626
+ /**
627
+ * Called when the peer connection is #ready.
628
+ *
629
+ * @private
630
+ * @example
631
+ */
632
+ #ready = () => {
633
+ // Perform callback with data
634
+ this.conn.on('data', this.#onIncomingData);
635
+ this.conn.on('close', () => {
636
+ console.log('Connection reset<br>Awaiting connection...');
637
+ this.conn = null;
638
+ });
639
+ };
640
+
641
+ /** .
642
+ * .
643
+ * .
644
+ * Debug method for downloading the recorded audio
645
+ *
646
+ * @public
647
+ * @example
648
+ */
649
+ downloadData = () => {
650
+ this.ac.downloadData();
651
+ };
652
+
653
+ repeatCalibration = async (params, stream, CalibratorInstance) => {
654
+ this.ac = CalibratorInstance;
655
+ this.#removeUIElems();
656
+ this.#showSpinner();
657
+
658
+ console.log('This is a repeat');
659
+ // wrap the calibration process in a promise so we can await it
660
+ return new Promise(async (resolve, reject) => {
661
+ const result = await this.ac.startCalibration(
662
+ stream,
663
+ params.gainValues,
664
+ params.ICalib,
665
+ params.knownIR,
666
+ params.microphoneName,
667
+ params.calibrateSoundCheck,
668
+ params.isSmartPhone,
669
+ params.calibrateSoundBurstDb,
670
+ params.calibrateSoundBurstFilteredExtraDb,
671
+ params.calibrateSoundBurstLevelReTBool,
672
+ params.calibrateSoundBurstUses1000HzGainBool,
673
+ params.calibrateSoundBurstRepeats,
674
+ params.calibrateSoundBurstSec,
675
+ params.calibrateSoundBurstsWarmup,
676
+ params.calibrateSoundHz,
677
+ params.calibrateSoundIRSec,
678
+ params.calibrateSoundIIRSec,
679
+ params.calibrateSoundIIRPhase,
680
+ params.calibrateSound1000HzPreSec,
681
+ params.calibrateSound1000HzSec,
682
+ params.calibrateSound1000HzPostSec,
683
+ params.calibrateSoundBackgroundSecs,
684
+ params.calibrateSoundSmoothOctaves,
685
+ params.calibrateSoundSmoothMinBandwidthHz,
686
+ params.calibrateSoundPowerBinDesiredSec,
687
+ params.calibrateSoundPowerDbSDToleratedDb,
688
+ params.calibrateSoundTaperSec,
689
+ params.micManufacturer,
690
+ params.micSerialNumber,
691
+ params.micModelNumber,
692
+ params.micModelName,
693
+ params.calibrateMicrophonesBool,
694
+ params.authorEmails,
695
+ params.webAudioDeviceNames,
696
+ params.IDsToSaveInSoundProfileLibrary,
697
+ params.restartButton,
698
+ params.reminder,
699
+ params.calibrateSoundLimit
700
+ );
701
+ this.#removeUIElems();
702
+ resolve(result);
703
+ });
704
+ };
705
+ }
706
+
707
+ /*
708
+ Referenced links:
709
+ https://stackoverflow.com/questions/28016664/when-you-pass-this-as-an-argument/28016676#28016676
710
+ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
711
+ https://stackoverflow.com/questions/879152/how-do-i-make-javascript-beep [3]
712
+ */
713
+
714
+ export default Speaker;