videomail-client 16.1.1 → 16.4.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/README.md +4 -6
- package/dist/cjs/index.cjs +2260 -1954
- package/dist/esm/index.js +2110 -1824
- package/dist/esm/types/events/params.d.ts +4 -3
- package/dist/esm/util/email/isRecipientSelectionValid.d.ts +4 -0
- package/dist/esm/util/html/media/AudioRecorder.d.ts +19 -9
- package/dist/esm/util/html/media/AudioRecorderWorklet.d.ts +17 -0
- package/dist/esm/wrappers/container.d.ts +1 -1
- package/dist/esm/wrappers/visuals/recorder/capture.d.ts +87 -0
- package/dist/esm/wrappers/visuals/recorder/connection.d.ts +46 -0
- package/dist/esm/wrappers/visuals/recorder/dimensions.d.ts +21 -0
- package/dist/esm/wrappers/visuals/recorder/media.d.ts +51 -0
- package/dist/esm/wrappers/visuals/recorder/mediaConstraints.d.ts +14 -0
- package/dist/esm/wrappers/visuals/recorder/progress.d.ts +11 -0
- package/dist/esm/wrappers/visuals/recorder/streamWriter.d.ts +56 -0
- package/dist/esm/wrappers/visuals/recorder.d.ts +13 -165
- package/dist/umd/index.js +2173 -1914
- package/package.json +12 -12
- /package/dist/esm/util/{trimEmail.d.ts → email/trimEmail.d.ts} +0 -0
package/dist/esm/index.js
CHANGED
|
@@ -3620,7 +3620,7 @@ const constants = {
|
|
|
3620
3620
|
}
|
|
3621
3621
|
};
|
|
3622
3622
|
var package_namespaceObject = {
|
|
3623
|
-
rE: "16.
|
|
3623
|
+
rE: "16.4.0"
|
|
3624
3624
|
};
|
|
3625
3625
|
function isAudioEnabled(options) {
|
|
3626
3626
|
return Boolean(options.audio.enabled);
|
|
@@ -4142,6 +4142,14 @@ class Despot {
|
|
|
4142
4142
|
}
|
|
4143
4143
|
Despot_define_property(Despot, "emitters", new WeakMap());
|
|
4144
4144
|
const util_Despot = Despot;
|
|
4145
|
+
const EMAIL_SEPARATOR_REGEX = /[ ,]+/gu;
|
|
4146
|
+
function trimEmail(email) {
|
|
4147
|
+
return email.replace(EMAIL_SEPARATOR_REGEX, "");
|
|
4148
|
+
}
|
|
4149
|
+
function trimEmails(emails) {
|
|
4150
|
+
const trimmedEmails = emails.split(EMAIL_SEPARATOR_REGEX).map((item)=>item.trim()).filter(Boolean);
|
|
4151
|
+
return trimmedEmails;
|
|
4152
|
+
}
|
|
4145
4153
|
function getValidity(element) {
|
|
4146
4154
|
const validity = "validity" in element ? element.validity : void 0;
|
|
4147
4155
|
return validity;
|
|
@@ -4161,14 +4169,6 @@ function showElement(element) {
|
|
|
4161
4169
|
element.style.removeProperty("display");
|
|
4162
4170
|
}
|
|
4163
4171
|
const html_showElement = showElement;
|
|
4164
|
-
const EMAIL_SEPARATOR_REGEX = /[ ,]+/gu;
|
|
4165
|
-
function trimEmail(email) {
|
|
4166
|
-
return email.replace(EMAIL_SEPARATOR_REGEX, "");
|
|
4167
|
-
}
|
|
4168
|
-
function trimEmails(emails) {
|
|
4169
|
-
const trimmedEmails = emails.split(EMAIL_SEPARATOR_REGEX).map((item)=>item.trim()).filter(Boolean);
|
|
4170
|
-
return trimmedEmails;
|
|
4171
|
-
}
|
|
4172
4172
|
function form_define_property(obj, key, value) {
|
|
4173
4173
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
4174
4174
|
value: value,
|
|
@@ -4876,6 +4876,19 @@ main_options.domAPI = styleDomAPI_default();
|
|
|
4876
4876
|
main_options.insertStyleElement = insertStyleElement_default();
|
|
4877
4877
|
injectStylesIntoStyleTag_default()(main_0.A, main_options);
|
|
4878
4878
|
main_0.A && main_0.A.locals && main_0.A.locals;
|
|
4879
|
+
function isRecipientSelectionValid(recipients) {
|
|
4880
|
+
const hasTo = Boolean(recipients.to?.length);
|
|
4881
|
+
const hasCc = Boolean(recipients.cc?.length);
|
|
4882
|
+
const hasBcc = Boolean(recipients.bcc?.length);
|
|
4883
|
+
const hasConfiguredTo = "to" in recipients;
|
|
4884
|
+
const hasConfiguredCc = "cc" in recipients;
|
|
4885
|
+
const hasConfiguredBcc = "bcc" in recipients;
|
|
4886
|
+
let isValid = true;
|
|
4887
|
+
if (hasConfiguredTo) isValid = hasTo || hasConfiguredCc && hasCc || hasConfiguredBcc && hasBcc;
|
|
4888
|
+
else if (hasConfiguredCc) isValid = hasCc || !hasConfiguredBcc || hasBcc;
|
|
4889
|
+
return isValid;
|
|
4890
|
+
}
|
|
4891
|
+
const email_isRecipientSelectionValid = isRecipientSelectionValid;
|
|
4879
4892
|
function limitHeight(height, options, calledFrom) {
|
|
4880
4893
|
const dimension = {
|
|
4881
4894
|
unit: "px"
|
|
@@ -5487,7 +5500,7 @@ class FacingMode extends util_Despot {
|
|
|
5487
5500
|
this.visuals = visuals;
|
|
5488
5501
|
}
|
|
5489
5502
|
}
|
|
5490
|
-
const
|
|
5503
|
+
const recorder_facingMode = FacingMode;
|
|
5491
5504
|
function pausedNote_define_property(obj, key, value) {
|
|
5492
5505
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
5493
5506
|
value: value,
|
|
@@ -5831,7 +5844,7 @@ class RecorderInsides extends util_Despot {
|
|
|
5831
5844
|
this.recordTimer = new recordTimer(visuals, this.recordNote, options);
|
|
5832
5845
|
const browser = util_getBrowser(options);
|
|
5833
5846
|
if (options.video.countdown) this.countdown = new countdown(visuals, options);
|
|
5834
|
-
if (options.video.facingModeButton && browser.isMobile()) this.facingMode = new
|
|
5847
|
+
if (options.video.facingModeButton && browser.isMobile()) this.facingMode = new recorder_facingMode(visuals, options);
|
|
5835
5848
|
if (options.enablePause) this.pausedNote = new pausedNote(visuals, options);
|
|
5836
5849
|
}
|
|
5837
5850
|
}
|
|
@@ -6078,46 +6091,18 @@ class Notifier extends util_Despot {
|
|
|
6078
6091
|
}
|
|
6079
6092
|
}
|
|
6080
6093
|
const notifier = Notifier;
|
|
6081
|
-
function
|
|
6082
|
-
|
|
6083
|
-
value: value
|
|
6084
|
-
|
|
6085
|
-
configurable: true,
|
|
6086
|
-
writable: true
|
|
6094
|
+
function summarize(title, contents) {
|
|
6095
|
+
const lines = Object.entries(contents).sort(([keyA], [keyB])=>keyA.localeCompare(keyB)).map(([key, value])=>{
|
|
6096
|
+
const formattedValue = void 0 === value ? "undefined" : "object" == typeof value ? string_pretty(value) : value;
|
|
6097
|
+
return ` • ${key}: ${formattedValue}`;
|
|
6087
6098
|
});
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
enqueue(buffer, params) {
|
|
6094
|
-
if (this.queuedBytes + buffer.length > this.maxBytes) return false;
|
|
6095
|
-
this.writes.push({
|
|
6096
|
-
buffer,
|
|
6097
|
-
params
|
|
6098
|
-
});
|
|
6099
|
-
this.queuedBytes += buffer.length;
|
|
6100
|
-
return true;
|
|
6101
|
-
}
|
|
6102
|
-
dequeue() {
|
|
6103
|
-
const write = this.writes.shift();
|
|
6104
|
-
if (write) this.queuedBytes -= write.buffer.length;
|
|
6105
|
-
return write;
|
|
6106
|
-
}
|
|
6107
|
-
clear() {
|
|
6108
|
-
this.writes.length = 0;
|
|
6109
|
-
this.queuedBytes = 0;
|
|
6110
|
-
}
|
|
6111
|
-
constructor(maxBytes = DEFAULT_MAX_QUEUED_BYTES){
|
|
6112
|
-
boundedWriteQueue_define_property(this, "maxBytes", void 0);
|
|
6113
|
-
boundedWriteQueue_define_property(this, "writes", void 0);
|
|
6114
|
-
boundedWriteQueue_define_property(this, "queuedBytes", void 0);
|
|
6115
|
-
this.maxBytes = maxBytes;
|
|
6116
|
-
this.writes = [];
|
|
6117
|
-
this.queuedBytes = 0;
|
|
6118
|
-
}
|
|
6099
|
+
const line = [
|
|
6100
|
+
`🔎 ${title}`,
|
|
6101
|
+
...lines
|
|
6102
|
+
].join("\n");
|
|
6103
|
+
return line;
|
|
6119
6104
|
}
|
|
6120
|
-
const
|
|
6105
|
+
const string_summarize = summarize;
|
|
6121
6106
|
function getEdgeCodes(value, amount, fromEnd) {
|
|
6122
6107
|
const chars = fromEnd ? value.slice(-amount) : value.slice(0, amount);
|
|
6123
6108
|
const codes = Array.from(chars, (char)=>String(char.codePointAt(0))).join(".");
|
|
@@ -6197,117 +6182,11 @@ function getWebSocketDiagnostic(url2Connect) {
|
|
|
6197
6182
|
};
|
|
6198
6183
|
}
|
|
6199
6184
|
const error_getWebSocketDiagnostic = getWebSocketDiagnostic;
|
|
6200
|
-
function figureMinHeight(height, options) {
|
|
6201
|
-
let minHeight;
|
|
6202
|
-
if (options.video.height) {
|
|
6203
|
-
minHeight = height ? Math.min(options.video.height, height) : options.video.height;
|
|
6204
|
-
if (minHeight < 1) throw error_createError({
|
|
6205
|
-
message: `Got a min height less than 1 (${minHeight})!`,
|
|
6206
|
-
options
|
|
6207
|
-
});
|
|
6208
|
-
} else minHeight = height;
|
|
6209
|
-
return minHeight;
|
|
6210
|
-
}
|
|
6211
|
-
const dimensions_figureMinHeight = figureMinHeight;
|
|
6212
|
-
function getRatio(options, videoHeight, videoWidth) {
|
|
6213
|
-
let ratio = 1;
|
|
6214
|
-
const hasVideoDimensions = videoHeight && videoWidth;
|
|
6215
|
-
const desiredHeight = options.video.height;
|
|
6216
|
-
const desiredWidth = options.video.width;
|
|
6217
|
-
const hasDesiredDimensions = desiredHeight && desiredWidth;
|
|
6218
|
-
if (hasDesiredDimensions) ratio = hasVideoDimensions ? videoHeight < desiredHeight || videoWidth < desiredWidth ? videoHeight / videoWidth : desiredHeight / desiredWidth : desiredHeight / desiredWidth;
|
|
6219
|
-
else if (hasVideoDimensions) ratio = videoHeight / videoWidth;
|
|
6220
|
-
return ratio;
|
|
6221
|
-
}
|
|
6222
|
-
const dimensions_getRatio = getRatio;
|
|
6223
|
-
function calculateHeight(responsive, videoWidth, options, ratio, element) {
|
|
6224
|
-
const dimension = {
|
|
6225
|
-
unit: "px"
|
|
6226
|
-
};
|
|
6227
|
-
let width = videoWidth;
|
|
6228
|
-
if (responsive && element) {
|
|
6229
|
-
const limitedDimension = dimensions_limitWidth(element, options, width);
|
|
6230
|
-
width = limitedDimension.value;
|
|
6231
|
-
}
|
|
6232
|
-
const chosenRatio = ratio ?? dimensions_getRatio(options, void 0, videoWidth);
|
|
6233
|
-
const height = width ? Math.round(width * chosenRatio) : void 0;
|
|
6234
|
-
if (height && Number.isInteger(height) && height < 1) throw error_createError({
|
|
6235
|
-
message: "Just calculated a height less than 1 which is wrong.",
|
|
6236
|
-
options
|
|
6237
|
-
});
|
|
6238
|
-
const minHeight = dimensions_figureMinHeight(height, options);
|
|
6239
|
-
dimension.value = minHeight;
|
|
6240
|
-
return dimension;
|
|
6241
|
-
}
|
|
6242
|
-
const dimensions_calculateHeight = calculateHeight;
|
|
6243
|
-
function calculateWidth(responsive, options, videoHeight, ratio) {
|
|
6244
|
-
const dimension = {
|
|
6245
|
-
unit: "px"
|
|
6246
|
-
};
|
|
6247
|
-
let height = dimensions_figureMinHeight(videoHeight, options);
|
|
6248
|
-
if (responsive) {
|
|
6249
|
-
const limitedDimension = dimensions_limitHeight(height, options, "calculateWidth");
|
|
6250
|
-
height = limitedDimension.value;
|
|
6251
|
-
}
|
|
6252
|
-
if (!height || height < 1) throw error_createError({
|
|
6253
|
-
message: `Height ${height} cannot be smaller than 1 when calculating width.`,
|
|
6254
|
-
options
|
|
6255
|
-
});
|
|
6256
|
-
const chosenRatio = ratio ?? dimensions_getRatio(options, videoHeight);
|
|
6257
|
-
const calculatedWidth = Math.round(height / chosenRatio);
|
|
6258
|
-
if (calculatedWidth < 1) throw error_createError({
|
|
6259
|
-
message: "Calculated width cannot be smaller than 1!",
|
|
6260
|
-
options
|
|
6261
|
-
});
|
|
6262
|
-
dimension.value = calculatedWidth;
|
|
6263
|
-
return dimension;
|
|
6264
|
-
}
|
|
6265
|
-
const dimensions_calculateWidth = calculateWidth;
|
|
6266
6185
|
function isAutomatedUserAgent(userAgent = navigator.userAgent) {
|
|
6267
6186
|
return /bot|crawler|spider|headlesschrome|phantomjs|puppeteer|playwright/iu.test(userAgent);
|
|
6268
6187
|
}
|
|
6269
6188
|
const util_isAutomatedUserAgent = isAutomatedUserAgent;
|
|
6270
|
-
function
|
|
6271
|
-
const lines = Object.entries(contents).sort(([keyA], [keyB])=>keyA.localeCompare(keyB)).map(([key, value])=>{
|
|
6272
|
-
const formattedValue = void 0 === value ? "undefined" : "object" == typeof value ? string_pretty(value) : value;
|
|
6273
|
-
return ` • ${key}: ${formattedValue}`;
|
|
6274
|
-
});
|
|
6275
|
-
const line = [
|
|
6276
|
-
`🔎 ${title}`,
|
|
6277
|
-
...lines
|
|
6278
|
-
].join("\n");
|
|
6279
|
-
return line;
|
|
6280
|
-
}
|
|
6281
|
-
const string_summarize = summarize;
|
|
6282
|
-
function getFirstVideoTrack(localMediaStream) {
|
|
6283
|
-
const videoTracks = localMediaStream.getVideoTracks();
|
|
6284
|
-
let videoTrack;
|
|
6285
|
-
if (videoTracks[0]) videoTrack = videoTracks[0];
|
|
6286
|
-
return videoTrack;
|
|
6287
|
-
}
|
|
6288
|
-
const media_getFirstVideoTrack = getFirstVideoTrack;
|
|
6289
|
-
const MEDIA_EVENTS = [
|
|
6290
|
-
"loadstart",
|
|
6291
|
-
"suspend",
|
|
6292
|
-
"progress",
|
|
6293
|
-
"abort",
|
|
6294
|
-
"emptied",
|
|
6295
|
-
"stalled",
|
|
6296
|
-
"pause",
|
|
6297
|
-
"loadeddata",
|
|
6298
|
-
"waiting",
|
|
6299
|
-
"playing",
|
|
6300
|
-
"canplay",
|
|
6301
|
-
"canplaythrough",
|
|
6302
|
-
"seeking",
|
|
6303
|
-
"seeked",
|
|
6304
|
-
"ended",
|
|
6305
|
-
"ratechange",
|
|
6306
|
-
"durationchange",
|
|
6307
|
-
"volumechange"
|
|
6308
|
-
];
|
|
6309
|
-
const mediaEvents = MEDIA_EVENTS;
|
|
6310
|
-
function AudioRecorder_define_property(obj, key, value) {
|
|
6189
|
+
function boundedWriteQueue_define_property(obj, key, value) {
|
|
6311
6190
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
6312
6191
|
value: value,
|
|
6313
6192
|
enumerable: true,
|
|
@@ -6317,120 +6196,223 @@ function AudioRecorder_define_property(obj, key, value) {
|
|
|
6317
6196
|
else obj[key] = value;
|
|
6318
6197
|
return obj;
|
|
6319
6198
|
}
|
|
6320
|
-
const
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6199
|
+
const DEFAULT_MAX_QUEUED_BYTES = 1048576;
|
|
6200
|
+
class BoundedWriteQueue {
|
|
6201
|
+
enqueue(buffer, params) {
|
|
6202
|
+
if (this.queuedBytes + buffer.length > this.maxBytes) return false;
|
|
6203
|
+
this.writes.push({
|
|
6204
|
+
buffer,
|
|
6205
|
+
params
|
|
6206
|
+
});
|
|
6207
|
+
this.queuedBytes += buffer.length;
|
|
6208
|
+
return true;
|
|
6327
6209
|
}
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
}
|
|
6333
|
-
return this.vcAudioContext;
|
|
6210
|
+
dequeue() {
|
|
6211
|
+
const write = this.writes.shift();
|
|
6212
|
+
if (write) this.queuedBytes -= write.buffer.length;
|
|
6213
|
+
return write;
|
|
6334
6214
|
}
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
cb(new audio_sample(float32Array));
|
|
6215
|
+
clear() {
|
|
6216
|
+
this.writes.length = 0;
|
|
6217
|
+
this.queuedBytes = 0;
|
|
6339
6218
|
}
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
this.
|
|
6346
|
-
this.
|
|
6219
|
+
constructor(maxBytes = DEFAULT_MAX_QUEUED_BYTES){
|
|
6220
|
+
boundedWriteQueue_define_property(this, "maxBytes", void 0);
|
|
6221
|
+
boundedWriteQueue_define_property(this, "writes", void 0);
|
|
6222
|
+
boundedWriteQueue_define_property(this, "queuedBytes", void 0);
|
|
6223
|
+
this.maxBytes = maxBytes;
|
|
6224
|
+
this.writes = [];
|
|
6225
|
+
this.queuedBytes = 0;
|
|
6347
6226
|
}
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6227
|
+
}
|
|
6228
|
+
const boundedWriteQueue = BoundedWriteQueue;
|
|
6229
|
+
function streamWriter_define_property(obj, key, value) {
|
|
6230
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
6231
|
+
value: value,
|
|
6232
|
+
enumerable: true,
|
|
6233
|
+
configurable: true,
|
|
6234
|
+
writable: true
|
|
6235
|
+
});
|
|
6236
|
+
else obj[key] = value;
|
|
6237
|
+
return obj;
|
|
6238
|
+
}
|
|
6239
|
+
const PIPE_SYMBOL = "°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸ ";
|
|
6240
|
+
class RecorderStreamWriter extends util_Despot {
|
|
6241
|
+
hasExceededWebSocketSendBuffer(buffer, opts) {
|
|
6242
|
+
return buffer.length > 0 && !this.pendingWrites.enqueue(buffer, opts);
|
|
6243
|
+
}
|
|
6244
|
+
writeStream(buffer, opts) {
|
|
6245
|
+
if (this.stream) if (this.stream.destroyed) {
|
|
6246
|
+
this.stopPings();
|
|
6247
|
+
const err = error_createError({
|
|
6248
|
+
message: "Already disconnected",
|
|
6249
|
+
explanation: "Sorry, connection to the server has been destroyed. Please reload.",
|
|
6359
6250
|
options: this.options
|
|
6360
6251
|
});
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
message: `Audio volume must be between zero and one. The current volume is ${this.options.audio.volume}.`,
|
|
6370
|
-
options: this.options
|
|
6371
|
-
});
|
|
6372
|
-
volume.gain.value = this.options.audio.volume;
|
|
6373
|
-
this.scriptProcessor = this.getAudioContext().createScriptProcessor(bufferSize, CHANNELS, CHANNELS);
|
|
6374
|
-
this.audioInput.connect(volume);
|
|
6375
|
-
volume.connect(this.scriptProcessor);
|
|
6376
|
-
this.scriptProcessor.connect(this.getAudioContext().destination);
|
|
6377
|
-
}
|
|
6378
|
-
record(cb) {
|
|
6379
|
-
this.options.logger.debug("AudioRecorder: record()");
|
|
6380
|
-
if (this.scriptProcessor) this.scriptProcessor.onaudioprocess = (e)=>{
|
|
6381
|
-
this.onAudioProcess(e, cb);
|
|
6382
|
-
};
|
|
6383
|
-
}
|
|
6384
|
-
async resume() {
|
|
6385
|
-
const context = this.getAudioContext();
|
|
6386
|
-
if ("running" !== context.state) await context.resume();
|
|
6252
|
+
this.handleWriteFailure(err);
|
|
6253
|
+
} else if (this.streamBackpressured) {
|
|
6254
|
+
if (this.hasExceededWebSocketSendBuffer(buffer, opts)) this.failMediaTransmission(error_createError({
|
|
6255
|
+
message: "WebSocket send buffer limit exceeded",
|
|
6256
|
+
explanation: "The connection to the server is too slow to keep up with this recording. The recording has been stopped; please start a new one.",
|
|
6257
|
+
options: this.options
|
|
6258
|
+
}));
|
|
6259
|
+
} else this.writeToStream(this.stream, buffer, opts);
|
|
6387
6260
|
}
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6261
|
+
writeToStream(stream, buffer, opts) {
|
|
6262
|
+
const onFlushedCallback = opts?.onFlushedCallback;
|
|
6263
|
+
try {
|
|
6264
|
+
const canContinue = stream.write(buffer, ()=>{
|
|
6265
|
+
if (!onFlushedCallback) return;
|
|
6266
|
+
try {
|
|
6267
|
+
onFlushedCallback(opts);
|
|
6268
|
+
} catch (exc) {
|
|
6269
|
+
const err = error_createError({
|
|
6270
|
+
message: "Failed to write stream buffer",
|
|
6271
|
+
explanation: `stream.write() failed because of ${string_pretty(exc)}`,
|
|
6272
|
+
options: this.options,
|
|
6273
|
+
exc
|
|
6274
|
+
});
|
|
6275
|
+
this.emit("ERROR", {
|
|
6276
|
+
err
|
|
6277
|
+
});
|
|
6278
|
+
}
|
|
6279
|
+
});
|
|
6280
|
+
if (!canContinue) {
|
|
6281
|
+
this.streamBackpressured = true;
|
|
6282
|
+
this.startBackpressureTimeout();
|
|
6283
|
+
this.pauseForBackpressure();
|
|
6284
|
+
return false;
|
|
6285
|
+
}
|
|
6286
|
+
return true;
|
|
6287
|
+
} catch (exc) {
|
|
6288
|
+
const err = error_createError({
|
|
6289
|
+
message: "Failed writing to server",
|
|
6290
|
+
explanation: `stream.write() failed because of ${string_pretty(exc)}`,
|
|
6291
|
+
options: this.options,
|
|
6292
|
+
exc
|
|
6293
|
+
});
|
|
6294
|
+
this.handleWriteFailure(err);
|
|
6295
|
+
return false;
|
|
6296
|
+
}
|
|
6297
|
+
}
|
|
6298
|
+
flushPendingWrites(stream) {
|
|
6299
|
+
if (this.isStaleStream(stream)) return;
|
|
6300
|
+
this.streamBackpressured = false;
|
|
6301
|
+
this.clearBackpressureTimeout();
|
|
6302
|
+
let pendingWrite = this.pendingWrites.dequeue();
|
|
6303
|
+
while(pendingWrite){
|
|
6304
|
+
if (!this.writeToStream(stream, pendingWrite.buffer, pendingWrite.params)) return;
|
|
6305
|
+
pendingWrite = this.pendingWrites.dequeue();
|
|
6306
|
+
}
|
|
6307
|
+
this.resumeRecordingAfterDrain();
|
|
6308
|
+
}
|
|
6309
|
+
isStaleStream(stream) {
|
|
6310
|
+
return this.stream !== stream || stream.destroyed;
|
|
6311
|
+
}
|
|
6312
|
+
canPauseForBackpressure() {
|
|
6313
|
+
return this.recordingActive && !this.isUserMediaPaused() && this.isCaptureLoopRunning();
|
|
6314
|
+
}
|
|
6315
|
+
pauseForBackpressure() {
|
|
6316
|
+
if (!this.canPauseForBackpressure()) return;
|
|
6317
|
+
this.resumeAfterDrain = true;
|
|
6318
|
+
this.stopCaptureLoop();
|
|
6319
|
+
}
|
|
6320
|
+
resumeRecordingAfterDrain() {
|
|
6321
|
+
if (!this.resumeAfterDrain) return;
|
|
6322
|
+
this.resumeAfterDrain = false;
|
|
6323
|
+
if (this.canResumeAfterDrain()) this.startCaptureLoop();
|
|
6324
|
+
}
|
|
6325
|
+
canResumeAfterDrain() {
|
|
6326
|
+
return this.recordingActive && !this.isUserMediaPaused();
|
|
6327
|
+
}
|
|
6328
|
+
handleWriteFailure(err) {
|
|
6329
|
+
if (this.mediaTransmissionStarted) return void this.failMediaTransmission(err);
|
|
6330
|
+
this.emit("ERROR", {
|
|
6331
|
+
err
|
|
6332
|
+
});
|
|
6333
|
+
}
|
|
6334
|
+
failMediaTransmission(err) {
|
|
6335
|
+
if (this.recordingTransportFailed) return;
|
|
6336
|
+
this.recordingTransportFailed = true;
|
|
6337
|
+
this.recordingActive = false;
|
|
6338
|
+
this.mediaTransmissionStarted = false;
|
|
6339
|
+
this.streamBackpressured = false;
|
|
6340
|
+
this.resumeAfterDrain = false;
|
|
6341
|
+
this.pendingWrites.clear();
|
|
6342
|
+
this.connected = false;
|
|
6343
|
+
this.connecting = false;
|
|
6344
|
+
this.reconnecting = false;
|
|
6345
|
+
this.clearReconnectTimeout();
|
|
6346
|
+
this.clearBackpressureTimeout();
|
|
6347
|
+
this.discardSocket();
|
|
6348
|
+
this.reset();
|
|
6349
|
+
this.framesCount = this.samplesCount = 0;
|
|
6350
|
+
this.confirmedFrameNumber = this.confirmedSampleNumber = 0;
|
|
6351
|
+
delete this.recordingStats;
|
|
6352
|
+
this.recordingDimensions = void 0;
|
|
6353
|
+
this.emit("ERROR", {
|
|
6354
|
+
err
|
|
6355
|
+
});
|
|
6356
|
+
}
|
|
6357
|
+
startBackpressureTimeout() {
|
|
6358
|
+
if (!this.shouldStartBackpressureTimeout()) return;
|
|
6359
|
+
this.backpressureTimeout = window.setTimeout(()=>{
|
|
6360
|
+
this.backpressureTimeout = void 0;
|
|
6361
|
+
if (this.shouldIgnoreBackpressureTimeout()) return;
|
|
6362
|
+
const err = error_createError({
|
|
6363
|
+
message: "WebSocket write timed out",
|
|
6364
|
+
explanation: "The server stopped accepting recording data. The recording has been stopped; please check your connection and start a new one.",
|
|
6397
6365
|
options: this.options
|
|
6398
|
-
})
|
|
6399
|
-
this.
|
|
6366
|
+
});
|
|
6367
|
+
this.failMediaTransmission(err);
|
|
6368
|
+
}, this.options.timeouts.connection);
|
|
6369
|
+
}
|
|
6370
|
+
shouldStartBackpressureTimeout() {
|
|
6371
|
+
return void 0 === this.backpressureTimeout && this.mediaTransmissionStarted;
|
|
6372
|
+
}
|
|
6373
|
+
shouldIgnoreBackpressureTimeout() {
|
|
6374
|
+
return !this.streamBackpressured || this.recordingTransportFailed;
|
|
6375
|
+
}
|
|
6376
|
+
failInterruptedRecording() {
|
|
6377
|
+
const unconfirmedFrames = Math.max(0, this.framesCount - this.confirmedFrameNumber);
|
|
6378
|
+
const unconfirmedSamples = Math.max(0, this.samplesCount - this.confirmedSampleNumber);
|
|
6379
|
+
const err = error_createError({
|
|
6380
|
+
message: "Recording interrupted",
|
|
6381
|
+
explanation: "The server connection closed before this recording completed. It cannot be safely resumed, so it has been stopped. Please start a new recording.",
|
|
6382
|
+
options: this.options,
|
|
6383
|
+
cause: {
|
|
6384
|
+
unconfirmedFrames,
|
|
6385
|
+
unconfirmedSamples
|
|
6386
|
+
}
|
|
6400
6387
|
});
|
|
6388
|
+
this.failMediaTransmission(err);
|
|
6401
6389
|
}
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6390
|
+
handleStreamDrain(stream) {
|
|
6391
|
+
this.options.logger.debug(`${PIPE_SYMBOL}Stream *drain* event emitted`);
|
|
6392
|
+
this.flushPendingWrites(stream);
|
|
6405
6393
|
}
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
this.
|
|
6414
|
-
this.
|
|
6394
|
+
clearBackpressureTimeout() {
|
|
6395
|
+
if (void 0 === this.backpressureTimeout) return;
|
|
6396
|
+
window.clearTimeout(this.backpressureTimeout);
|
|
6397
|
+
this.backpressureTimeout = void 0;
|
|
6398
|
+
}
|
|
6399
|
+
discardSocket() {
|
|
6400
|
+
this.options.logger.debug("Recorder: discarding socket stream ...");
|
|
6401
|
+
this.clearBackpressureTimeout();
|
|
6402
|
+
this.pendingWrites.clear();
|
|
6403
|
+
this.streamBackpressured = false;
|
|
6404
|
+
this.resumeAfterDrain = false;
|
|
6405
|
+
const stream = this.stream;
|
|
6406
|
+
this.stream = void 0;
|
|
6407
|
+
if (stream && !stream.destroyed) stream.destroy();
|
|
6408
|
+
}
|
|
6409
|
+
constructor(options){
|
|
6410
|
+
super("Recorder", options), streamWriter_define_property(this, "pendingWrites", new boundedWriteQueue()), streamWriter_define_property(this, "samplesCount", 0), streamWriter_define_property(this, "framesCount", 0), streamWriter_define_property(this, "recordingStats", void 0), streamWriter_define_property(this, "confirmedFrameNumber", 0), streamWriter_define_property(this, "confirmedSampleNumber", 0), streamWriter_define_property(this, "backpressureTimeout", void 0), streamWriter_define_property(this, "recordingDimensions", void 0), streamWriter_define_property(this, "stream", void 0), streamWriter_define_property(this, "streamBackpressured", false), streamWriter_define_property(this, "resumeAfterDrain", false), streamWriter_define_property(this, "mediaTransmissionStarted", false), streamWriter_define_property(this, "recordingActive", false), streamWriter_define_property(this, "recordingTransportFailed", false), streamWriter_define_property(this, "connecting", false), streamWriter_define_property(this, "connected", false), streamWriter_define_property(this, "reconnecting", false);
|
|
6415
6411
|
}
|
|
6416
6412
|
}
|
|
6417
|
-
const
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
"virtual",
|
|
6421
|
-
"manycam",
|
|
6422
|
-
"vcam",
|
|
6423
|
-
"splitcam",
|
|
6424
|
-
"droidcam"
|
|
6425
|
-
];
|
|
6426
|
-
function isVirtualCamera(videoTrack) {
|
|
6427
|
-
const capabilities = videoTrack.getCapabilities();
|
|
6428
|
-
if (!capabilities.frameRate) return true;
|
|
6429
|
-
const isVirtual = VIRTUAL_KEYWORDS.some((keyword)=>videoTrack.label.toLowerCase().includes(keyword));
|
|
6430
|
-
return isVirtual;
|
|
6431
|
-
}
|
|
6432
|
-
const util_isVirtualCamera = isVirtualCamera;
|
|
6433
|
-
function userMedia_define_property(obj, key, value) {
|
|
6413
|
+
const streamWriter = RecorderStreamWriter;
|
|
6414
|
+
var Buffer = __webpack_require__("dY").hp;
|
|
6415
|
+
function connection_define_property(obj, key, value) {
|
|
6434
6416
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
6435
6417
|
value: value,
|
|
6436
6418
|
enumerable: true,
|
|
@@ -6440,345 +6422,687 @@ function userMedia_define_property(obj, key, value) {
|
|
|
6440
6422
|
else obj[key] = value;
|
|
6441
6423
|
return obj;
|
|
6442
6424
|
}
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
if (this.rawVisualUserMedia) {
|
|
6447
|
-
this.rawVisualUserMedia.srcObject = stream;
|
|
6448
|
-
const previousStream = this.currentVisualStream;
|
|
6449
|
-
this.currentVisualStream = stream;
|
|
6450
|
-
if (previousStream && previousStream !== stream) previousStream.getTracks().forEach((track)=>{
|
|
6451
|
-
track.stop();
|
|
6452
|
-
});
|
|
6453
|
-
} else throw error_createError({
|
|
6454
|
-
message: "Error attaching stream to element.",
|
|
6455
|
-
explanation: "Contact the developer about this",
|
|
6456
|
-
options: this.options
|
|
6457
|
-
});
|
|
6458
|
-
}
|
|
6459
|
-
setVisualStream(localMediaStream) {
|
|
6460
|
-
if (localMediaStream) this.attachMediaStream(localMediaStream);
|
|
6461
|
-
else {
|
|
6462
|
-
if (this.rawVisualUserMedia) {
|
|
6463
|
-
this.rawVisualUserMedia.srcObject = null;
|
|
6464
|
-
this.rawVisualUserMedia.removeAttribute("src");
|
|
6465
|
-
}
|
|
6466
|
-
this.currentVisualStream = void 0;
|
|
6467
|
-
}
|
|
6468
|
-
}
|
|
6469
|
-
async restorePlayback() {
|
|
6470
|
-
if (!this.rawVisualUserMedia) throw new Error("No camera preview is available.");
|
|
6471
|
-
await this.rawVisualUserMedia.play();
|
|
6472
|
-
await this.audioRecorder?.resume();
|
|
6473
|
-
}
|
|
6474
|
-
hasLiveStream() {
|
|
6475
|
-
const videoReady = this.currentVisualStream?.getVideoTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
|
|
6476
|
-
const audioReady = !isAudioEnabled(this.options) || this.currentVisualStream?.getAudioTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
|
|
6477
|
-
return Boolean(this.currentVisualStream?.active && videoReady && audioReady);
|
|
6478
|
-
}
|
|
6479
|
-
hasEnded() {
|
|
6480
|
-
if (this.rawVisualUserMedia?.ended) return this.rawVisualUserMedia.ended;
|
|
6481
|
-
return !this.currentVisualStream?.active;
|
|
6482
|
-
}
|
|
6483
|
-
hasInvalidDimensions() {
|
|
6484
|
-
if (!this.rawVisualUserMedia) return false;
|
|
6485
|
-
const { videoWidth, videoHeight } = this.rawVisualUserMedia;
|
|
6486
|
-
return videoWidth > 0 && videoWidth < 3 || videoHeight > 0 && videoHeight < 3;
|
|
6425
|
+
class RecorderConnection extends streamWriter {
|
|
6426
|
+
canReconnectAfterStreamClose() {
|
|
6427
|
+
return Boolean(this.connected && this.userMediaLoaded);
|
|
6487
6428
|
}
|
|
6488
|
-
|
|
6489
|
-
this.
|
|
6429
|
+
shouldReportStreamClose() {
|
|
6430
|
+
return !this.connecting && !this.blocking;
|
|
6490
6431
|
}
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
this.
|
|
6432
|
+
handleStreamClose(url2Connect) {
|
|
6433
|
+
const debugLine = string_summarize(`${PIPE_SYMBOL}Stream has closed:`, {
|
|
6434
|
+
connecting: this.connecting,
|
|
6435
|
+
connected: this.connected,
|
|
6436
|
+
userMediaLoaded: this.userMediaLoaded,
|
|
6437
|
+
blocking: this.blocking
|
|
6496
6438
|
});
|
|
6497
|
-
this.
|
|
6439
|
+
this.options.logger.debug(debugLine);
|
|
6440
|
+
const canReconnect = this.canReconnectAfterStreamClose();
|
|
6441
|
+
this.connected = false;
|
|
6442
|
+
this.stopPings();
|
|
6443
|
+
if (this.shouldIgnoreStreamClose()) return;
|
|
6444
|
+
if (this.mediaTransmissionStarted) this.failInterruptedRecording();
|
|
6445
|
+
else if (canReconnect) {
|
|
6446
|
+
this.pendingWrites.clear();
|
|
6447
|
+
this.streamBackpressured = false;
|
|
6448
|
+
this.resumeAfterDrain = false;
|
|
6449
|
+
this.reconnecting = true;
|
|
6450
|
+
this.initSocket();
|
|
6451
|
+
} else if (this.shouldReportStreamClose()) window.setTimeout(()=>{
|
|
6452
|
+
this.handleConnectionFailure({
|
|
6453
|
+
url2Connect,
|
|
6454
|
+
cause: "closed"
|
|
6455
|
+
});
|
|
6456
|
+
}, 0);
|
|
6498
6457
|
}
|
|
6499
|
-
|
|
6500
|
-
this.
|
|
6501
|
-
this.audioRecorder?.record(audioCallback);
|
|
6458
|
+
shouldIgnoreStreamClose() {
|
|
6459
|
+
return this.recordingTransportFailed || Boolean(this.unloaded) || this.validate();
|
|
6502
6460
|
}
|
|
6503
|
-
|
|
6504
|
-
this.
|
|
6505
|
-
this.
|
|
6506
|
-
|
|
6507
|
-
|
|
6461
|
+
handleStreamError(url2Connect, err) {
|
|
6462
|
+
if ("u" > typeof Event && err instanceof Event) this.lastSocketError = error_getEventDetails(err);
|
|
6463
|
+
else this.lastSocketError = serializeError(err);
|
|
6464
|
+
const debugLine = string_summarize(`${PIPE_SYMBOL}Stream *error* event emitted:`, {
|
|
6465
|
+
error: serializeError(err)
|
|
6508
6466
|
});
|
|
6509
|
-
|
|
6510
|
-
this.
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6467
|
+
this.options.logger.debug(debugLine);
|
|
6468
|
+
if (this.validate()) {
|
|
6469
|
+
this.connected = false;
|
|
6470
|
+
this.stopPings();
|
|
6471
|
+
this.discardSocket();
|
|
6472
|
+
return;
|
|
6473
|
+
}
|
|
6474
|
+
if (!this.connected) return void this.handleConnectionFailure({
|
|
6475
|
+
url2Connect,
|
|
6476
|
+
cause: "error"
|
|
6477
|
+
});
|
|
6478
|
+
const streamError = err instanceof Error ? err : new Error("WebSocket stream emitted an error event", {
|
|
6479
|
+
cause: this.lastSocketError ?? err
|
|
6480
|
+
});
|
|
6481
|
+
if (this.mediaTransmissionStarted) return void this.failInterruptedRecording();
|
|
6482
|
+
this.emit("ERROR", {
|
|
6483
|
+
err: error_createError({
|
|
6484
|
+
message: "WebSocket stream error",
|
|
6485
|
+
explanation: `The WebSocket stream emitted an error event. Details: ${string_pretty(err)}`,
|
|
6486
|
+
options: this.options,
|
|
6487
|
+
exc: streamError
|
|
6488
|
+
})
|
|
6489
|
+
});
|
|
6490
|
+
}
|
|
6491
|
+
sendPings() {
|
|
6492
|
+
this.stopPings();
|
|
6493
|
+
this.pingInterval = window.setInterval(()=>{
|
|
6494
|
+
this.options.logger.debug("Recorder: pinging...");
|
|
6495
|
+
this.writeStream(Buffer.from(""));
|
|
6496
|
+
}, this.options.timeouts.pingInterval);
|
|
6497
|
+
}
|
|
6498
|
+
stopPings() {
|
|
6499
|
+
if (void 0 === this.pingInterval) return;
|
|
6500
|
+
this.options.logger.debug("Recorder: stopping pings...");
|
|
6501
|
+
window.clearInterval(this.pingInterval);
|
|
6502
|
+
this.pingInterval = void 0;
|
|
6503
|
+
}
|
|
6504
|
+
clearConnectionTimeout() {
|
|
6505
|
+
if (void 0 === this.connectionTimeout) return;
|
|
6506
|
+
this.options.logger.debug("Recorder: clearConnectionTimeout()");
|
|
6507
|
+
window.clearTimeout(this.connectionTimeout);
|
|
6508
|
+
this.connectionTimeout = void 0;
|
|
6509
|
+
}
|
|
6510
|
+
clearReconnectTimeout() {
|
|
6511
|
+
if (void 0 === this.reconnectTimeout) return;
|
|
6512
|
+
this.options.logger.debug("Recorder: clearReconnectTimeout()");
|
|
6513
|
+
window.clearTimeout(this.reconnectTimeout);
|
|
6514
|
+
this.reconnectTimeout = void 0;
|
|
6515
|
+
}
|
|
6516
|
+
handleConnectionFailure(params) {
|
|
6517
|
+
if (this.retryConnection(params)) return;
|
|
6518
|
+
this.failConnection(params);
|
|
6519
|
+
}
|
|
6520
|
+
retryConnection(params) {
|
|
6521
|
+
if (!this.canRetryConnection()) return false;
|
|
6522
|
+
if (void 0 !== this.reconnectTimeout) return true;
|
|
6523
|
+
const delayMs = Math.min(1000 * 2 ** this.reconnectAttempts, 10000);
|
|
6524
|
+
this.reconnectAttempts++;
|
|
6525
|
+
this.connecting = false;
|
|
6526
|
+
this.clearConnectionTimeout();
|
|
6527
|
+
this.discardSocket();
|
|
6528
|
+
this.options.logger.debug(`Recorder: retrying WebSocket connection in ${delayMs}ms (cause ${params.cause}).`);
|
|
6529
|
+
this.reconnectTimeout = window.setTimeout(()=>{
|
|
6530
|
+
this.reconnectTimeout = void 0;
|
|
6531
|
+
if (this.shouldContinueReconnect()) this.initSocket();
|
|
6532
|
+
}, delayMs);
|
|
6533
|
+
return true;
|
|
6534
|
+
}
|
|
6535
|
+
canRetryConnection() {
|
|
6536
|
+
return Boolean(this.reconnecting && this.userMediaLoaded && this.isOnline() && !this.unloaded && !this.blocking);
|
|
6537
|
+
}
|
|
6538
|
+
shouldContinueReconnect() {
|
|
6539
|
+
return this.reconnecting && !this.unloaded && !this.blocking;
|
|
6540
|
+
}
|
|
6541
|
+
isOnline() {
|
|
6542
|
+
return navigator.onLine;
|
|
6543
|
+
}
|
|
6544
|
+
failConnection(params) {
|
|
6545
|
+
if (this.shouldIgnoreConnectionFailure()) return;
|
|
6546
|
+
this.connectionFailed = true;
|
|
6547
|
+
this.connecting = false;
|
|
6548
|
+
this.reconnecting = false;
|
|
6549
|
+
this.reconnectAttempts = 0;
|
|
6550
|
+
this.clearReconnectTimeout();
|
|
6551
|
+
this.clearConnectionTimeout();
|
|
6552
|
+
const { url2Connect, cause } = params;
|
|
6553
|
+
const online = this.isOnline();
|
|
6554
|
+
const elapsedMs = this.connectingStartedAt ? Date.now() - this.connectingStartedAt : void 0;
|
|
6555
|
+
const closeEvent = this.lastCloseEvent;
|
|
6556
|
+
const socketError = this.lastSocketError;
|
|
6557
|
+
const contents = {
|
|
6558
|
+
cause,
|
|
6559
|
+
closeCode: closeEvent?.code,
|
|
6560
|
+
closeReason: closeEvent?.reason,
|
|
6561
|
+
elapsedMs,
|
|
6562
|
+
online,
|
|
6563
|
+
secureContext: globalThis.isSecureContext,
|
|
6564
|
+
socketReadyState: this.stream?.socket.readyState,
|
|
6565
|
+
timeoutMs: this.options.timeouts.connection,
|
|
6566
|
+
url: url2Connect,
|
|
6567
|
+
unloaded: this.unloaded,
|
|
6568
|
+
userMediaLoaded: this.userMediaLoaded,
|
|
6569
|
+
userMediaLoading: this.userMediaLoading,
|
|
6570
|
+
wasClean: closeEvent?.wasClean,
|
|
6571
|
+
socketError,
|
|
6572
|
+
blocking: this.blocking,
|
|
6573
|
+
visibilityState: document.visibilityState
|
|
6526
6574
|
};
|
|
6527
|
-
const
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6575
|
+
const debugLine = string_summarize("Recorder: failConnection() diagnostic", contents);
|
|
6576
|
+
this.options.logger.debug(debugLine);
|
|
6577
|
+
let explanation;
|
|
6578
|
+
if (online) if ("timeout" === cause) explanation = `The server at ${url2Connect} did not respond within ${this.options.timeouts.connection}ms, even though your device is online. This usually points to a firewall or proxy silently dropping the connection. Please try a different network. If the problem persists, contact us.`;
|
|
6579
|
+
else if ("error" === cause) explanation = `An error occurred while connecting to ${url2Connect}. Please check your internet connection and try again. If the problem persists, contact us.`;
|
|
6580
|
+
else {
|
|
6581
|
+
const closeSuffix = closeEvent ? ` (code ${closeEvent.code})` : "";
|
|
6582
|
+
explanation = `Connection to ${url2Connect} is closed${closeSuffix}. Please check your internet connection and try again. If the problem persists, contact us.`;
|
|
6583
|
+
}
|
|
6584
|
+
else explanation = "Your device appears to be offline. Please check your internet connection and try again.";
|
|
6585
|
+
this.discardSocket();
|
|
6586
|
+
const err = error_createError({
|
|
6587
|
+
message: "Unable to connect to the server",
|
|
6588
|
+
explanation,
|
|
6589
|
+
options: this.options,
|
|
6590
|
+
cause: contents,
|
|
6591
|
+
exc: socketError ? new Error("WebSocket connection error", {
|
|
6592
|
+
cause: socketError
|
|
6593
|
+
}) : void 0
|
|
6594
|
+
});
|
|
6595
|
+
this.emit("ERROR", {
|
|
6596
|
+
err
|
|
6597
|
+
});
|
|
6598
|
+
}
|
|
6599
|
+
shouldIgnoreConnectionFailure() {
|
|
6600
|
+
return this.connectionFailed || this.connected || Boolean(this.unloaded) || this.validate();
|
|
6601
|
+
}
|
|
6602
|
+
initSocket(cb) {
|
|
6603
|
+
if (this.connected) {
|
|
6604
|
+
this.options.logger.debug("Recorder: already **connected**. Not going to initialize a new WebSocket connection.");
|
|
6605
|
+
cb?.();
|
|
6606
|
+
return;
|
|
6607
|
+
}
|
|
6608
|
+
if (this.connecting) return void this.options.logger.debug("Recorder: already **connecting**. Not going to initialize a new WebSocket connection.");
|
|
6609
|
+
this.clearReconnectTimeout();
|
|
6610
|
+
if (util_isAutomatedUserAgent()) {
|
|
6611
|
+
this.connecting = false;
|
|
6612
|
+
this.options.logger.debug("Recorder: skipping web socket connection for an automated crawler");
|
|
6613
|
+
return;
|
|
6614
|
+
}
|
|
6615
|
+
this.connecting = true;
|
|
6616
|
+
this.connectionFailed = false;
|
|
6617
|
+
this.connectingStartedAt = Date.now();
|
|
6618
|
+
this.lastCloseEvent = void 0;
|
|
6619
|
+
this.lastSocketError = void 0;
|
|
6620
|
+
this.emit("CONNECTING");
|
|
6621
|
+
let url2Connect;
|
|
6622
|
+
try {
|
|
6623
|
+
const socketUrlObj = new URL(this.options.socketUrl);
|
|
6624
|
+
socketUrlObj.searchParams.set(constants.WHITELIST_KEY_LABEL, this.options.whitelistKey);
|
|
6625
|
+
url2Connect = socketUrlObj.toString();
|
|
6626
|
+
} catch (exc) {
|
|
6627
|
+
this.connecting = this.connected = false;
|
|
6628
|
+
const err = error_createError({
|
|
6629
|
+
message: "Invalid WebSocket URL",
|
|
6630
|
+
explanation: `The configured socketUrl "${this.options.socketUrl}" is not a valid URL. Please check your videomail-client configuration.`,
|
|
6631
|
+
options: this.options,
|
|
6632
|
+
exc
|
|
6633
|
+
});
|
|
6634
|
+
this.emit("ERROR", {
|
|
6635
|
+
err
|
|
6636
|
+
});
|
|
6637
|
+
return;
|
|
6638
|
+
}
|
|
6639
|
+
this.options.logger.debug(`Recorder: initializing web socket stream to ${url2Connect}`);
|
|
6640
|
+
let nativeSocket;
|
|
6641
|
+
try {
|
|
6642
|
+
nativeSocket = new WebSocket(url2Connect);
|
|
6643
|
+
nativeSocket.addEventListener("close", (event)=>{
|
|
6644
|
+
this.lastCloseEvent = {
|
|
6645
|
+
code: event.code,
|
|
6646
|
+
reason: event.reason,
|
|
6647
|
+
wasClean: event.wasClean
|
|
6648
|
+
};
|
|
6649
|
+
});
|
|
6650
|
+
nativeSocket.addEventListener("error", (event)=>{
|
|
6651
|
+
this.lastSocketError = error_getEventDetails(event);
|
|
6652
|
+
});
|
|
6653
|
+
} catch (exc) {
|
|
6654
|
+
this.connecting = this.connected = false;
|
|
6655
|
+
const diagnostic = error_getWebSocketDiagnostic(url2Connect);
|
|
6656
|
+
const message = diagnostic.looksAutomated ? "Automated crawler: WebSocket not supported in this environment" : `Failed to construct WebSocket to ${url2Connect}`;
|
|
6657
|
+
const explanation = diagnostic.looksAutomated ? "Headless crawlers cannot use Videomail's WebSocket. No action needed." : `Please check your connection and try again. If the problem persists, contact us. Diagnostic: ${diagnostic.text}`;
|
|
6658
|
+
const err = error_createError({
|
|
6659
|
+
message,
|
|
6660
|
+
explanation,
|
|
6661
|
+
options: this.options,
|
|
6662
|
+
exc
|
|
6663
|
+
});
|
|
6664
|
+
this.emit("ERROR", {
|
|
6665
|
+
err
|
|
6666
|
+
});
|
|
6667
|
+
return;
|
|
6668
|
+
}
|
|
6669
|
+
try {
|
|
6670
|
+
this.stream = websocket_stream(nativeSocket);
|
|
6671
|
+
} catch (exc) {
|
|
6672
|
+
this.connecting = this.connected = false;
|
|
6673
|
+
const err = error_createError({
|
|
6674
|
+
message: `Failed to create a stream to ${url2Connect}`,
|
|
6675
|
+
explanation: "Please check your connection and try again. If the problem persists, contact us.",
|
|
6676
|
+
options: this.options,
|
|
6677
|
+
exc
|
|
6678
|
+
});
|
|
6679
|
+
this.emit("ERROR", {
|
|
6680
|
+
err
|
|
6681
|
+
});
|
|
6682
|
+
}
|
|
6683
|
+
const stream = this.stream;
|
|
6684
|
+
if (stream) {
|
|
6685
|
+
const connectionTimeoutMs = this.options.timeouts.connection;
|
|
6686
|
+
this.connectionTimeout = window.setTimeout(()=>{
|
|
6687
|
+
if (this.isOnline() && this.isUserMediaLoaded()) {
|
|
6688
|
+
this.options.logger.debug(`${PIPE_SYMBOL}Reconnecting due to connection timeout.`);
|
|
6689
|
+
this.reconnecting = true;
|
|
6690
|
+
this.handleConnectionFailure({
|
|
6691
|
+
url2Connect,
|
|
6692
|
+
cause: "timeout"
|
|
6549
6693
|
});
|
|
6694
|
+
} else this.handleConnectionFailure({
|
|
6695
|
+
url2Connect,
|
|
6696
|
+
cause: "timeout"
|
|
6697
|
+
});
|
|
6698
|
+
}, connectionTimeoutMs);
|
|
6699
|
+
stream.on("close", ()=>{
|
|
6700
|
+
this.handleStreamClose(url2Connect);
|
|
6701
|
+
});
|
|
6702
|
+
stream.on("connect", (args)=>{
|
|
6703
|
+
this.options.logger.debug(`${PIPE_SYMBOL}Stream *connect* event emitted with args: ${string_pretty(args)}`);
|
|
6704
|
+
this.clearConnectionTimeout();
|
|
6705
|
+
const isClosing = this.stream?.socket.readyState === WebSocket.CLOSING;
|
|
6706
|
+
if (!this.connected && !isClosing && !this.unloaded) {
|
|
6707
|
+
this.connected = true;
|
|
6708
|
+
this.connecting = this.unloaded = false;
|
|
6709
|
+
this.reconnecting = false;
|
|
6710
|
+
this.reconnectAttempts = 0;
|
|
6711
|
+
this.clearReconnectTimeout();
|
|
6712
|
+
this.emit("CONNECTED");
|
|
6713
|
+
cb?.();
|
|
6550
6714
|
}
|
|
6551
|
-
}
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6715
|
+
});
|
|
6716
|
+
stream.on("data", (data)=>{
|
|
6717
|
+
this.options.logger.debug(`${PIPE_SYMBOL}Stream *data* event emitted`);
|
|
6718
|
+
try {
|
|
6719
|
+
const command = JSON.parse(data.toString());
|
|
6720
|
+
this.executeCommand(command);
|
|
6721
|
+
} catch (exc) {
|
|
6722
|
+
this.options.logger.error(`Failed to parse command: ${exc}`);
|
|
6723
|
+
const err = error_createError({
|
|
6724
|
+
message: "Invalid server command",
|
|
6725
|
+
explanation: `Contact us. The invalid command was: ${data.toString()}.`,
|
|
6726
|
+
options: this.options,
|
|
6727
|
+
exc
|
|
6728
|
+
});
|
|
6729
|
+
this.emit("ERROR", {
|
|
6730
|
+
err
|
|
6566
6731
|
});
|
|
6567
|
-
this.clearInitializationListeners?.();
|
|
6568
|
-
videoCallback();
|
|
6569
|
-
};
|
|
6570
|
-
if (params?.recovering) this.audioRecorder.resume().then(onAudioReady).catch(endedEarlyCallback);
|
|
6571
|
-
else onAudioReady();
|
|
6572
|
-
} catch (exc) {
|
|
6573
|
-
unloadAllEventListeners();
|
|
6574
|
-
this.stop(void 0, {
|
|
6575
|
-
preserveRecordingState: Boolean(params?.recovering)
|
|
6576
|
-
});
|
|
6577
|
-
endedEarlyCallback(exc);
|
|
6578
|
-
}
|
|
6579
|
-
else {
|
|
6580
|
-
this.clearInitializationListeners?.();
|
|
6581
|
-
videoCallback();
|
|
6582
|
-
}
|
|
6583
|
-
};
|
|
6584
|
-
const onPlay = ()=>{
|
|
6585
|
-
try {
|
|
6586
|
-
this.logEvent("play", {
|
|
6587
|
-
readyState: this.rawVisualUserMedia?.readyState,
|
|
6588
|
-
audio: isAudioEnabled(this.options),
|
|
6589
|
-
width: this.rawVisualUserMedia?.width,
|
|
6590
|
-
height: this.rawVisualUserMedia?.height,
|
|
6591
|
-
videoWidth: this.rawVisualUserMedia?.videoWidth,
|
|
6592
|
-
videoHeight: this.rawVisualUserMedia?.videoHeight
|
|
6593
|
-
});
|
|
6594
|
-
this.rawVisualUserMedia?.removeEventListener("play", onPlay);
|
|
6595
|
-
if (this.hasEnded() || this.hasInvalidDimensions()) endedEarlyCallback(error_createError({
|
|
6596
|
-
message: "Already busy",
|
|
6597
|
-
explanation: "Probably another browser window is using your webcam?",
|
|
6598
|
-
options: this.options
|
|
6599
|
-
}));
|
|
6600
|
-
else {
|
|
6601
|
-
this.onPlayReached = true;
|
|
6602
|
-
fireCallbacks();
|
|
6603
6732
|
}
|
|
6604
|
-
} catch (exc) {
|
|
6605
|
-
unloadAllEventListeners();
|
|
6606
|
-
endedEarlyCallback(exc);
|
|
6607
|
-
}
|
|
6608
|
-
};
|
|
6609
|
-
const onLoadedMetaData = ()=>{
|
|
6610
|
-
this.logEvent("loadedmetadata", {
|
|
6611
|
-
readyState: this.rawVisualUserMedia?.readyState,
|
|
6612
|
-
paused: this.rawVisualUserMedia?.paused,
|
|
6613
|
-
width: this.rawVisualUserMedia?.width,
|
|
6614
|
-
height: this.rawVisualUserMedia?.height,
|
|
6615
|
-
videoWidth: this.rawVisualUserMedia?.videoWidth,
|
|
6616
|
-
videoHeight: this.rawVisualUserMedia?.videoHeight
|
|
6617
6733
|
});
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
}
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
const videoTrack = media_getFirstVideoTrack(localMediaStream);
|
|
6627
|
-
this.videoTrackLabel = videoTrack?.label;
|
|
6628
|
-
if (videoTrack) if (videoTrack.enabled) if (util_isVirtualCamera(videoTrack)) throw error_createError({
|
|
6629
|
-
message: "Virtual cameras are not allowed.",
|
|
6630
|
-
explanation: `Please use a real camera, not "${videoTrack.label}".`,
|
|
6631
|
-
options: this.options
|
|
6734
|
+
stream.on("error", (err)=>{
|
|
6735
|
+
this.handleStreamError(url2Connect, err);
|
|
6736
|
+
});
|
|
6737
|
+
stream.on("drain", ()=>{
|
|
6738
|
+
this.handleStreamDrain(stream);
|
|
6739
|
+
});
|
|
6740
|
+
stream.on("preend", (args)=>{
|
|
6741
|
+
this.options.logger.debug(`${PIPE_SYMBOL}Stream *preend* event emitted with args: ${string_pretty(args)}`);
|
|
6632
6742
|
});
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
this.options.logger.debug(
|
|
6743
|
+
stream.on("end", ()=>{
|
|
6744
|
+
this.options.logger.debug(`${PIPE_SYMBOL}Stream *end* event emitted`);
|
|
6745
|
+
});
|
|
6746
|
+
stream.on("pipe", (src)=>{
|
|
6747
|
+
this.options.logger.debug(`${PIPE_SYMBOL}Stream *pipe* event emitted with src: ${string_pretty(src)}`);
|
|
6748
|
+
});
|
|
6749
|
+
stream.on("unpipe", (src)=>{
|
|
6750
|
+
this.options.logger.debug(`${PIPE_SYMBOL}Stream *unpipe* event emitted with src: ${string_pretty(src)}`);
|
|
6751
|
+
});
|
|
6752
|
+
stream.on("resume", (args)=>{
|
|
6753
|
+
this.options.logger.debug(`${PIPE_SYMBOL}Stream *resume* event emitted with args: ${string_pretty(args)}`);
|
|
6754
|
+
});
|
|
6755
|
+
stream.on("uncork", (args)=>{
|
|
6756
|
+
this.options.logger.debug(`${PIPE_SYMBOL}Stream *uncork* event emitted with args: ${string_pretty(args)}`);
|
|
6757
|
+
});
|
|
6758
|
+
stream.on("readable", (args)=>{
|
|
6759
|
+
this.options.logger.debug(`${PIPE_SYMBOL}Stream *readable* event emitted with args: ${string_pretty(args)}`);
|
|
6760
|
+
});
|
|
6761
|
+
stream.on("prefinish", (args)=>{
|
|
6762
|
+
this.options.logger.debug(`${PIPE_SYMBOL}Stream *prefinish* event emitted with args: ${string_pretty(args)}`);
|
|
6763
|
+
});
|
|
6764
|
+
stream.on("finish", (args)=>{
|
|
6765
|
+
this.options.logger.debug(`${PIPE_SYMBOL}Stream *finish* event emitted with args: ${string_pretty(args)}`);
|
|
6766
|
+
});
|
|
6767
|
+
}
|
|
6768
|
+
}
|
|
6769
|
+
writeCommand(command, args, cb) {
|
|
6770
|
+
if (this.connected) {
|
|
6771
|
+
if (this.stream) {
|
|
6772
|
+
if (args) this.options.logger.debug(`$ ${command} with ${string_pretty(args)}`);
|
|
6773
|
+
else this.options.logger.debug(`$ ${command}`);
|
|
6774
|
+
const commandObj = {
|
|
6775
|
+
command,
|
|
6776
|
+
args
|
|
6777
|
+
};
|
|
6778
|
+
this.writeStream(Buffer.from(JSON.stringify(commandObj)), cb ? {
|
|
6779
|
+
onFlushedCallback: ()=>{
|
|
6780
|
+
queueMicrotask(cb);
|
|
6781
|
+
}
|
|
6782
|
+
} : void 0);
|
|
6638
6783
|
}
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6784
|
+
} else {
|
|
6785
|
+
this.options.logger.debug(`Reconnecting for the command ${command} …`);
|
|
6786
|
+
this.initSocket(()=>{
|
|
6787
|
+
this.writeCommand(command, args, cb);
|
|
6643
6788
|
});
|
|
6644
|
-
else this.options.logger.debug("UserMedia: detected (but no video tracks exist");
|
|
6645
|
-
this.rawVisualUserMedia?.addEventListener("loadedmetadata", onLoadedMetaData);
|
|
6646
|
-
this.rawVisualUserMedia?.addEventListener("play", onPlay);
|
|
6647
|
-
this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
|
|
6648
|
-
this.rawVisualUserMedia?.addEventListener("error", this.handleMediaError);
|
|
6649
|
-
this.setVisualStream(localMediaStream);
|
|
6650
|
-
play();
|
|
6651
|
-
} catch (exc) {
|
|
6652
|
-
unloadAllEventListeners();
|
|
6653
|
-
endedEarlyCallback(exc);
|
|
6654
6789
|
}
|
|
6655
6790
|
}
|
|
6656
|
-
|
|
6657
|
-
|
|
6791
|
+
constructor(options){
|
|
6792
|
+
super(options), connection_define_property(this, "reconnectTimeout", void 0), connection_define_property(this, "connectionTimeout", void 0), connection_define_property(this, "userMediaLoaded", void 0), connection_define_property(this, "userMediaLoading", false), connection_define_property(this, "unloaded", void 0), connection_define_property(this, "connectionFailed", false), connection_define_property(this, "reconnectAttempts", 0), connection_define_property(this, "blocking", false), connection_define_property(this, "connectingStartedAt", void 0), connection_define_property(this, "lastCloseEvent", void 0), connection_define_property(this, "lastSocketError", void 0), connection_define_property(this, "pingInterval", void 0);
|
|
6658
6793
|
}
|
|
6659
|
-
|
|
6660
|
-
|
|
6794
|
+
}
|
|
6795
|
+
const connection = RecorderConnection;
|
|
6796
|
+
function createUserMediaConstraints({ options, facingMode, limitedWidth, userMediaParams }) {
|
|
6797
|
+
const video = {
|
|
6798
|
+
frameRate: {
|
|
6799
|
+
ideal: options.video.fps
|
|
6800
|
+
}
|
|
6801
|
+
};
|
|
6802
|
+
if (userMediaParams?.switchingFacingMode || userMediaParams?.recovering) video.facingMode = userMediaParams.switchingFacingMode || facingMode;
|
|
6803
|
+
if (options.video.width) video.width = {
|
|
6804
|
+
ideal: options.video.width
|
|
6805
|
+
};
|
|
6806
|
+
else if (limitedWidth?.value) video.width = {
|
|
6807
|
+
ideal: limitedWidth.value
|
|
6808
|
+
};
|
|
6809
|
+
if (options.video.height) video.height = {
|
|
6810
|
+
ideal: options.video.height
|
|
6811
|
+
};
|
|
6812
|
+
return {
|
|
6813
|
+
audio: isAudioEnabled(options),
|
|
6814
|
+
video
|
|
6815
|
+
};
|
|
6816
|
+
}
|
|
6817
|
+
const mediaConstraints = createUserMediaConstraints;
|
|
6818
|
+
function media_define_property(obj, key, value) {
|
|
6819
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
6820
|
+
value: value,
|
|
6821
|
+
enumerable: true,
|
|
6822
|
+
configurable: true,
|
|
6823
|
+
writable: true
|
|
6824
|
+
});
|
|
6825
|
+
else obj[key] = value;
|
|
6826
|
+
return obj;
|
|
6827
|
+
}
|
|
6828
|
+
class RecorderMedia extends connection {
|
|
6829
|
+
show() {
|
|
6830
|
+
html_showElement(this.recorderElement);
|
|
6661
6831
|
}
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6832
|
+
clearRetryTimeout() {
|
|
6833
|
+
if (void 0 === this.retryTimeout) return;
|
|
6834
|
+
this.options.logger.debug("Recorder: clearRetryTimeout()");
|
|
6835
|
+
window.clearTimeout(this.retryTimeout);
|
|
6836
|
+
this.retryTimeout = void 0;
|
|
6837
|
+
}
|
|
6838
|
+
showUserMedia() {
|
|
6839
|
+
if (!this.connected) return false;
|
|
6840
|
+
const hidden = this.isHidden();
|
|
6841
|
+
if (!hidden) return true;
|
|
6842
|
+
const notifying = this.isNotifying();
|
|
6843
|
+
if (notifying) return true;
|
|
6844
|
+
return this.blocking;
|
|
6845
|
+
}
|
|
6846
|
+
userMediaErrorCallback(err, usedConstraints) {
|
|
6847
|
+
if (this.cameraRecovery) return void this.failCameraRecovery(err);
|
|
6848
|
+
if (this.unloaded) return void this.options.logger.debug(`Recorder: already unloaded. Not going to throw error ${string_pretty(err)}`);
|
|
6849
|
+
this.userMediaLoading = false;
|
|
6850
|
+
this.clearUserMediaTimeout();
|
|
6851
|
+
const characteristics = this.userMedia?.getCharacteristics();
|
|
6852
|
+
this.options.logger.debug(`Recorder: userMediaErrorCallback(), name: ${err.name}, message: ${err.message} and Webcam characteristics: ${characteristics ? string_pretty(characteristics) : "none"}`);
|
|
6853
|
+
const errorListeners = this.getListeners("ERROR");
|
|
6854
|
+
if (errorListeners?.length) if (err.name !== error_VideomailError.MEDIA_DEVICE_NOT_SUPPORTED) {
|
|
6855
|
+
const videomailError = error_createError({
|
|
6856
|
+
err,
|
|
6857
|
+
options: this.options,
|
|
6858
|
+
usedConstraints
|
|
6859
|
+
});
|
|
6860
|
+
this.emit("ERROR", {
|
|
6861
|
+
err: videomailError
|
|
6862
|
+
});
|
|
6863
|
+
} else {
|
|
6864
|
+
this.options.logger.debug(`Recorder: ignore user media error ${string_pretty(err)}`);
|
|
6865
|
+
this.retryTimeout = window.setTimeout(()=>{
|
|
6866
|
+
this.retryTimeout = void 0;
|
|
6867
|
+
if (!this.unloaded) this.loadUserMedia();
|
|
6868
|
+
}, this.options.timeouts.userMedia);
|
|
6869
|
+
}
|
|
6870
|
+
else {
|
|
6871
|
+
this.options.logger.debug(`Recorder: no error listeners attached but throwing error ${string_pretty(err)}`);
|
|
6872
|
+
throw error_createError({
|
|
6873
|
+
err,
|
|
6874
|
+
message: "Unable to process this error since there are no error listeners anymore.",
|
|
6875
|
+
options: this.options
|
|
6876
|
+
});
|
|
6877
|
+
}
|
|
6878
|
+
}
|
|
6879
|
+
getUserMediaCallback(localStream, params, requestId = this.mediaRequestId) {
|
|
6880
|
+
if (!this.userMedia) {
|
|
6881
|
+
localStream.getTracks().forEach((track)=>{
|
|
6882
|
+
track.stop();
|
|
6883
|
+
});
|
|
6884
|
+
throw error_createError({
|
|
6885
|
+
message: "No user media is defined",
|
|
6886
|
+
options: this.options
|
|
6887
|
+
});
|
|
6888
|
+
}
|
|
6889
|
+
this.options.logger.debug(`Recorder: getUserMediaCallback(${params ? string_pretty(params) : ""})`);
|
|
6890
|
+
if (this.showUserMedia()) try {
|
|
6891
|
+
this.userMedia.init(localStream, ()=>{
|
|
6892
|
+
if (!this.unloaded && requestId === this.mediaRequestId) this.onUserMediaReady(params);
|
|
6893
|
+
}, this.onAudioSample.bind(this), (err)=>{
|
|
6894
|
+
if (!this.unloaded && requestId === this.mediaRequestId) this.userMediaErrorCallback(err, {
|
|
6895
|
+
audio: isAudioEnabled(this.options)
|
|
6672
6896
|
});
|
|
6673
|
-
|
|
6674
|
-
this.audioRecorder?.stop();
|
|
6675
|
-
this.audioRecorder = void 0;
|
|
6676
|
-
}
|
|
6677
|
-
if (!params?.preserveRecordingState) this.paused = this.recording = false;
|
|
6897
|
+
}, params);
|
|
6678
6898
|
} catch (exc) {
|
|
6679
|
-
|
|
6680
|
-
|
|
6899
|
+
localStream.getTracks().forEach((track)=>{
|
|
6900
|
+
track.stop();
|
|
6901
|
+
});
|
|
6902
|
+
this.userMediaLoading = false;
|
|
6903
|
+
this.clearUserMediaTimeout();
|
|
6904
|
+
this.userMediaErrorCallback(exc, {
|
|
6905
|
+
audio: isAudioEnabled(this.options)
|
|
6906
|
+
});
|
|
6907
|
+
}
|
|
6908
|
+
else {
|
|
6909
|
+
localStream.getTracks().forEach((track)=>{
|
|
6910
|
+
track.stop();
|
|
6681
6911
|
});
|
|
6912
|
+
this.userMediaLoading = false;
|
|
6913
|
+
this.clearUserMediaTimeout();
|
|
6914
|
+
this.cancelCameraRecovery();
|
|
6682
6915
|
}
|
|
6683
6916
|
}
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
const
|
|
6689
|
-
|
|
6690
|
-
|
|
6917
|
+
loadGenuineUserMedia(params) {
|
|
6918
|
+
this.options.logger.debug(`Recorder: loadGenuineUserMedia(${params ? string_pretty(params) : ""})`);
|
|
6919
|
+
this.emit("ASKING_WEBCAM_PERMISSION");
|
|
6920
|
+
this.userMediaLoading = true;
|
|
6921
|
+
const requestId = ++this.mediaRequestId;
|
|
6922
|
+
const limitedWidth = this.options.video.width ? void 0 : this.visuals.limitWidth();
|
|
6923
|
+
const constraints = mediaConstraints({
|
|
6924
|
+
facingMode: this.facingMode,
|
|
6925
|
+
limitedWidth,
|
|
6926
|
+
options: this.options,
|
|
6927
|
+
userMediaParams: params
|
|
6928
|
+
});
|
|
6929
|
+
this.options.logger.debug(string_summarize("Recorder: our webcam constraints are:", constraints));
|
|
6930
|
+
this.options.logger.debug(string_summarize("Recorder: available webcam constraints are:", navigator.mediaDevices.getSupportedConstraints()));
|
|
6931
|
+
const streamPromise = navigator.mediaDevices.getUserMedia(constraints);
|
|
6932
|
+
streamPromise.then((localStream)=>{
|
|
6933
|
+
if (this.unloaded || requestId !== this.mediaRequestId) return void localStream.getTracks().forEach((track)=>{
|
|
6934
|
+
track.stop();
|
|
6935
|
+
});
|
|
6936
|
+
this.getUserMediaCallback(localStream, params, requestId);
|
|
6937
|
+
}).catch((reason)=>{
|
|
6938
|
+
if (!this.unloaded && requestId === this.mediaRequestId) this.userMediaErrorCallback(reason, constraints);
|
|
6939
|
+
});
|
|
6691
6940
|
}
|
|
6692
|
-
|
|
6693
|
-
if (
|
|
6694
|
-
|
|
6941
|
+
loadUserMedia(params) {
|
|
6942
|
+
if (this.userMediaLoaded) {
|
|
6943
|
+
this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already loaded");
|
|
6944
|
+
this.onUserMediaReady(params);
|
|
6945
|
+
return;
|
|
6946
|
+
} else if (this.userMediaLoading) return void this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already asking for permission");
|
|
6947
|
+
this.options.logger.debug(`Recorder: loadUserMedia(${params ? string_pretty(params) : ""})`);
|
|
6948
|
+
this.emit("LOADING_USER_MEDIA");
|
|
6949
|
+
try {
|
|
6950
|
+
if (!navigator.mediaDevices) throw new Error("No media devices are available.");
|
|
6951
|
+
this.userMediaTimeout = window.setTimeout(()=>{
|
|
6952
|
+
if (!this.userMediaLoaded) {
|
|
6953
|
+
this.userMediaLoading = false;
|
|
6954
|
+
const err = util_getBrowser(this.options).getNoAccessIssue();
|
|
6955
|
+
this.userMediaErrorCallback(err, {
|
|
6956
|
+
audio: isAudioEnabled(this.options)
|
|
6957
|
+
});
|
|
6958
|
+
}
|
|
6959
|
+
}, this.options.timeouts.userMedia);
|
|
6960
|
+
this.userMediaLoading = true;
|
|
6961
|
+
this.loadGenuineUserMedia(params);
|
|
6962
|
+
} catch (exc) {
|
|
6963
|
+
this.clearUserMediaTimeout();
|
|
6964
|
+
this.options.logger.debug("Recorder: failed to load genuine user media");
|
|
6965
|
+
this.userMediaLoading = false;
|
|
6966
|
+
if (this.cameraRecovery) return void this.failCameraRecovery(exc);
|
|
6967
|
+
const errorListeners = this.getListeners("ERROR");
|
|
6968
|
+
if (errorListeners?.length) this.emit("ERROR", {
|
|
6969
|
+
exc
|
|
6970
|
+
});
|
|
6971
|
+
else {
|
|
6972
|
+
this.options.logger.debug("Recorder: no error listeners attached but throwing exception further");
|
|
6973
|
+
throw exc;
|
|
6974
|
+
}
|
|
6975
|
+
}
|
|
6695
6976
|
}
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
return this.rawVisualUserMedia.videoWidth || this.rawVisualUserMedia.width;
|
|
6977
|
+
isNotifying() {
|
|
6978
|
+
return this.visuals.isNotifying();
|
|
6699
6979
|
}
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
return videoWidth && videoWidth > 0;
|
|
6980
|
+
isHidden() {
|
|
6981
|
+
return !this.recorderElement || html_isHidden(this.recorderElement);
|
|
6703
6982
|
}
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
rawWidth = responsive ? this.getVideoWidth() : this.options.video.width ? this.options.video.width : this.getVideoWidth();
|
|
6710
|
-
if (responsive) {
|
|
6711
|
-
const widthDimension = this.recorder.limitWidth(rawWidth);
|
|
6712
|
-
rawWidth = widthDimension?.value;
|
|
6983
|
+
clearUserMediaTimeout() {
|
|
6984
|
+
if (void 0 !== this.userMediaTimeout) {
|
|
6985
|
+
this.options.logger.debug("Recorder: clearUserMediaTimeout()");
|
|
6986
|
+
window.clearTimeout(this.userMediaTimeout);
|
|
6987
|
+
this.userMediaTimeout = void 0;
|
|
6713
6988
|
}
|
|
6714
|
-
widthDimension.value = rawWidth;
|
|
6715
|
-
return widthDimension;
|
|
6716
6989
|
}
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
const ratio = this.recorder.getRatio();
|
|
6722
|
-
const rawWidth = this.getRawWidth(responsive);
|
|
6723
|
-
if (void 0 !== ratio && rawWidth.value) rawHeight = rawWidth.value * ratio;
|
|
6724
|
-
} else rawHeight = this.options.video.height ? this.options.video.height : this.getVideoHeight();
|
|
6725
|
-
if (responsive) {
|
|
6726
|
-
const heightDimension = this.recorder.limitHeight(rawHeight);
|
|
6727
|
-
rawHeight = heightDimension.value;
|
|
6728
|
-
}
|
|
6729
|
-
return rawHeight;
|
|
6990
|
+
clearStopTimeout() {
|
|
6991
|
+
if (void 0 === this.stopTimeout) return;
|
|
6992
|
+
window.clearTimeout(this.stopTimeout);
|
|
6993
|
+
this.stopTimeout = void 0;
|
|
6730
6994
|
}
|
|
6731
|
-
|
|
6732
|
-
return this.
|
|
6995
|
+
isReady() {
|
|
6996
|
+
return this.userMedia?.isReady();
|
|
6733
6997
|
}
|
|
6734
|
-
|
|
6735
|
-
this.
|
|
6998
|
+
finishCameraRecovery(restored) {
|
|
6999
|
+
this.clearUserMediaTimeout();
|
|
7000
|
+
const resolve = this.resolveCameraRecovery;
|
|
7001
|
+
const resumeRequested = restored && this.resumeAfterCameraRecovery;
|
|
7002
|
+
this.cameraRecovery = void 0;
|
|
7003
|
+
this.resolveCameraRecovery = void 0;
|
|
7004
|
+
this.resumeAfterCameraRecovery = false;
|
|
7005
|
+
resolve?.(restored);
|
|
7006
|
+
if (resumeRequested && this.isPaused()) this.resume();
|
|
6736
7007
|
}
|
|
6737
|
-
|
|
6738
|
-
|
|
7008
|
+
cancelCameraRecovery() {
|
|
7009
|
+
if (!this.cameraRecovery) return;
|
|
7010
|
+
this.mediaRequestId++;
|
|
7011
|
+
this.userMediaLoading = false;
|
|
7012
|
+
this.userMediaLoaded = false;
|
|
7013
|
+
this.clearUserMediaTimeout();
|
|
7014
|
+
this.userMedia?.stop(void 0, {
|
|
7015
|
+
preserveRecordingState: true
|
|
7016
|
+
});
|
|
7017
|
+
this.finishCameraRecovery(false);
|
|
6739
7018
|
}
|
|
6740
|
-
|
|
6741
|
-
this.
|
|
7019
|
+
failCameraRecovery(exc) {
|
|
7020
|
+
this.cancelCameraRecovery();
|
|
7021
|
+
const err = error_createError({
|
|
7022
|
+
message: "Camera recovery failed",
|
|
7023
|
+
explanation: "Your recording is still paused. Check camera permissions, then try Resume again or stop to keep the recorded part.",
|
|
7024
|
+
exc,
|
|
7025
|
+
options: this.options
|
|
7026
|
+
});
|
|
7027
|
+
this.emit("CAMERA_RECOVERY_FAILED", {
|
|
7028
|
+
err
|
|
7029
|
+
});
|
|
6742
7030
|
}
|
|
6743
|
-
|
|
6744
|
-
this.
|
|
7031
|
+
recoverCamera() {
|
|
7032
|
+
if (this.cameraRecovery) return this.cameraRecovery;
|
|
7033
|
+
if (!this.built || this.unloaded || !this.userMedia || this.userMediaLoading || this.submitting || this.key || void 0 !== this.stopTimeout || this.recordingTransportFailed) return Promise.resolve(false);
|
|
7034
|
+
const liveStream = this.userMedia.hasLiveStream();
|
|
7035
|
+
if (liveStream && this.isRecording()) return Promise.resolve(true);
|
|
7036
|
+
if (!liveStream && this.recordingActive && !this.isPaused()) this.pause();
|
|
7037
|
+
const recovery = new Promise((resolve)=>{
|
|
7038
|
+
this.resolveCameraRecovery = resolve;
|
|
7039
|
+
});
|
|
7040
|
+
this.cameraRecovery = recovery;
|
|
7041
|
+
this.emit("CAMERA_RECOVERING");
|
|
7042
|
+
if (liveStream) {
|
|
7043
|
+
this.userMediaTimeout = window.setTimeout(()=>{
|
|
7044
|
+
this.failCameraRecovery(new Error("Camera playback recovery timed out."));
|
|
7045
|
+
}, this.options.timeouts.userMedia);
|
|
7046
|
+
this.userMedia.restorePlayback().then(()=>{
|
|
7047
|
+
if (this.cameraRecovery === recovery) this.finishCameraRecovery(true);
|
|
7048
|
+
}).catch((exc)=>{
|
|
7049
|
+
if (this.cameraRecovery === recovery) this.failCameraRecovery(exc);
|
|
7050
|
+
});
|
|
7051
|
+
} else {
|
|
7052
|
+
this.userMediaLoaded = false;
|
|
7053
|
+
this.loadUserMedia({
|
|
7054
|
+
recovering: true
|
|
7055
|
+
});
|
|
7056
|
+
}
|
|
7057
|
+
return recovery;
|
|
6745
7058
|
}
|
|
6746
|
-
|
|
6747
|
-
|
|
7059
|
+
switchFacingMode() {
|
|
7060
|
+
if (this.cameraRecovery || !util_getBrowser(this.options).isMobile()) return;
|
|
7061
|
+
if ("user" === this.facingMode) this.facingMode = "environment";
|
|
7062
|
+
else if ("environment" === this.facingMode) this.facingMode = "user";
|
|
7063
|
+
else this.options.logger.warn(`Recorder: unsupported facing mode ${string_pretty(this.facingMode)}`);
|
|
7064
|
+
this.loadGenuineUserMedia({
|
|
7065
|
+
switchingFacingMode: this.facingMode
|
|
7066
|
+
});
|
|
6748
7067
|
}
|
|
6749
|
-
|
|
6750
|
-
if (this.
|
|
6751
|
-
|
|
7068
|
+
hide() {
|
|
7069
|
+
if (!this.isHidden()) {
|
|
7070
|
+
if (this.recorderElement) html_hideElement(this.recorderElement);
|
|
7071
|
+
this.clearUserMediaTimeout();
|
|
7072
|
+
this.clearRetryTimeout();
|
|
7073
|
+
}
|
|
6752
7074
|
}
|
|
6753
|
-
|
|
6754
|
-
return
|
|
6755
|
-
audioSampleRate: this.getAudioSampleRate(),
|
|
6756
|
-
muted: this.rawVisualUserMedia?.muted,
|
|
6757
|
-
width: this.rawVisualUserMedia?.width,
|
|
6758
|
-
height: this.rawVisualUserMedia?.height,
|
|
6759
|
-
videoWidth: this.rawVisualUserMedia?.videoWidth,
|
|
6760
|
-
videoHeight: this.rawVisualUserMedia?.videoHeight
|
|
6761
|
-
};
|
|
7075
|
+
isUnloaded() {
|
|
7076
|
+
return this.unloaded;
|
|
6762
7077
|
}
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
this.options.logger.warn(`Caught video element error event: ${string_pretty(event)}`);
|
|
6771
|
-
});
|
|
6772
|
-
this.recorder = recorder;
|
|
6773
|
-
this.rawVisualUserMedia = recorder.getRawVisualUserMedia();
|
|
6774
|
-
mediaEvents.forEach((eventName)=>{
|
|
6775
|
-
this.rawVisualUserMedia?.addEventListener(eventName, this.outputEvent, false);
|
|
6776
|
-
});
|
|
7078
|
+
isUserMediaLoaded() {
|
|
7079
|
+
return this.userMediaLoaded;
|
|
7080
|
+
}
|
|
7081
|
+
constructor(visuals, options){
|
|
7082
|
+
super(options), media_define_property(this, "visuals", void 0), media_define_property(this, "recorderElement", void 0), media_define_property(this, "userMedia", void 0), media_define_property(this, "userMediaTimeout", void 0), media_define_property(this, "retryTimeout", void 0), media_define_property(this, "stopTimeout", void 0), media_define_property(this, "mediaRequestId", 0), media_define_property(this, "cameraRecovery", void 0), media_define_property(this, "resolveCameraRecovery", void 0), media_define_property(this, "resumeAfterCameraRecovery", false), media_define_property(this, "submitting", false), media_define_property(this, "built", false), media_define_property(this, "key", void 0), media_define_property(this, "facingMode", void 0);
|
|
7083
|
+
this.visuals = visuals;
|
|
7084
|
+
this.facingMode = options.video.facingMode;
|
|
6777
7085
|
}
|
|
6778
7086
|
}
|
|
6779
|
-
const
|
|
6780
|
-
|
|
6781
|
-
|
|
7087
|
+
const media = RecorderMedia;
|
|
7088
|
+
function calculateProgress(confirmedCount, totalCount) {
|
|
7089
|
+
return `${(confirmedCount / (totalCount || 1) * 100).toFixed(2)}%`;
|
|
7090
|
+
}
|
|
7091
|
+
function getConfirmedProgressNumber({ args, kind, options }) {
|
|
7092
|
+
const label = "frame" === kind ? "frame" : "audio sample";
|
|
7093
|
+
if (!args) throw error_createError({
|
|
7094
|
+
message: `Arguments are missing for updating the ${label} progress`,
|
|
7095
|
+
options
|
|
7096
|
+
});
|
|
7097
|
+
const confirmedNumber = args[kind];
|
|
7098
|
+
if (!confirmedNumber) throw error_createError({
|
|
7099
|
+
message: `The ${label} number is missing`,
|
|
7100
|
+
options
|
|
7101
|
+
});
|
|
7102
|
+
return confirmedNumber;
|
|
7103
|
+
}
|
|
7104
|
+
var capture_Buffer = __webpack_require__("dY").hp;
|
|
7105
|
+
function capture_define_property(obj, key, value) {
|
|
6782
7106
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
6783
7107
|
value: value,
|
|
6784
7108
|
enumerable: true,
|
|
@@ -6788,1139 +7112,1319 @@ function recorder_define_property(obj, key, value) {
|
|
|
6788
7112
|
else obj[key] = value;
|
|
6789
7113
|
return obj;
|
|
6790
7114
|
}
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
message: "Already disconnected",
|
|
6801
|
-
explanation: "Sorry, connection to the server has been destroyed. Please reload.",
|
|
6802
|
-
options: this.options
|
|
6803
|
-
});
|
|
6804
|
-
this.handleWriteFailure(err);
|
|
6805
|
-
} else if (this.streamBackpressured) {
|
|
6806
|
-
if (this.hasExceededWebSocketSendBuffer(buffer, opts)) this.failMediaTransmission(error_createError({
|
|
6807
|
-
message: "WebSocket send buffer limit exceeded",
|
|
6808
|
-
explanation: "The connection to the server is too slow to keep up with this recording. The recording has been stopped; please start a new one.",
|
|
6809
|
-
options: this.options
|
|
6810
|
-
}));
|
|
6811
|
-
} else this.writeToStream(this.stream, buffer, opts);
|
|
7115
|
+
class RecorderCapture extends media {
|
|
7116
|
+
onAudioSample(audioSample) {
|
|
7117
|
+
if (this.recordingTransportFailed || this.cameraRecovery) return;
|
|
7118
|
+
const sampleRate = this.userMedia?.getAudioSampleRate();
|
|
7119
|
+
if (sampleRate && sampleRate > 0) this.recordingAudioSampleRate ??= sampleRate;
|
|
7120
|
+
this.samplesCount++;
|
|
7121
|
+
this.mediaTransmissionStarted = true;
|
|
7122
|
+
const audioBuffer = audioSample.toBuffer();
|
|
7123
|
+
this.writeStream(audioBuffer);
|
|
6812
7124
|
}
|
|
6813
|
-
|
|
6814
|
-
const onFlushedCallback = opts?.onFlushedCallback;
|
|
7125
|
+
onUserMediaReady(params) {
|
|
6815
7126
|
try {
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
const err = error_createError({
|
|
6822
|
-
message: "Failed to write stream buffer",
|
|
6823
|
-
explanation: `stream.write() failed because of ${string_pretty(exc)}`,
|
|
6824
|
-
options: this.options,
|
|
6825
|
-
exc
|
|
6826
|
-
});
|
|
6827
|
-
this.emit("ERROR", {
|
|
6828
|
-
err
|
|
6829
|
-
});
|
|
6830
|
-
}
|
|
6831
|
-
});
|
|
6832
|
-
if (!canContinue) {
|
|
6833
|
-
this.streamBackpressured = true;
|
|
6834
|
-
this.startBackpressureTimeout();
|
|
6835
|
-
this.pauseForBackpressure();
|
|
6836
|
-
return false;
|
|
7127
|
+
this.options.logger.debug(`Recorder: onUserMediaReady(${params ? string_pretty(params) : ""})`);
|
|
7128
|
+
const switchingFacingMode = params?.switchingFacingMode;
|
|
7129
|
+
if (params?.recovering) {
|
|
7130
|
+
if (!this.userMedia?.hasLiveStream()) return void this.failCameraRecovery(new Error("The restored camera has no live tracks."));
|
|
7131
|
+
if (void 0 !== this.recordingAudioSampleRate && this.userMedia.getAudioSampleRate() !== this.recordingAudioSampleRate) return void this.failCameraRecovery(new Error("The audio sample rate changed during recovery."));
|
|
6837
7132
|
}
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
7133
|
+
this.userMediaLoading = this.blocking = this.unloaded = this.submitting = false;
|
|
7134
|
+
this.userMediaLoaded = true;
|
|
7135
|
+
this.clearUserMediaTimeout();
|
|
7136
|
+
if (!switchingFacingMode && !params?.recovering) this.loop = this.createLoop();
|
|
7137
|
+
this.show();
|
|
7138
|
+
if (params?.recordWhenReady) this.record();
|
|
7139
|
+
this.emit("USER_MEDIA_READY", {
|
|
7140
|
+
switchingFacingMode: params?.switchingFacingMode,
|
|
7141
|
+
recovering: params?.recovering,
|
|
7142
|
+
paused: this.isPaused(),
|
|
7143
|
+
recordWhenReady: params?.recordWhenReady
|
|
7144
|
+
});
|
|
7145
|
+
if (params?.recovering) this.finishCameraRecovery(true);
|
|
7146
|
+
} catch (exc) {
|
|
7147
|
+
if (params?.recovering) this.failCameraRecovery(exc);
|
|
7148
|
+
else this.emit("ERROR", {
|
|
6844
7149
|
exc
|
|
6845
7150
|
});
|
|
6846
|
-
this.handleWriteFailure(err);
|
|
6847
|
-
return false;
|
|
6848
|
-
}
|
|
6849
|
-
}
|
|
6850
|
-
flushPendingWrites(stream) {
|
|
6851
|
-
if (this.isStaleStream(stream)) return;
|
|
6852
|
-
this.streamBackpressured = false;
|
|
6853
|
-
this.clearBackpressureTimeout();
|
|
6854
|
-
let pendingWrite = this.pendingWrites.dequeue();
|
|
6855
|
-
while(pendingWrite){
|
|
6856
|
-
if (!this.writeToStream(stream, pendingWrite.buffer, pendingWrite.params)) return;
|
|
6857
|
-
pendingWrite = this.pendingWrites.dequeue();
|
|
6858
7151
|
}
|
|
6859
|
-
this.resumeRecordingAfterDrain();
|
|
6860
|
-
}
|
|
6861
|
-
isStaleStream(stream) {
|
|
6862
|
-
return this.stream !== stream || stream.destroyed;
|
|
6863
7152
|
}
|
|
6864
|
-
|
|
6865
|
-
return
|
|
7153
|
+
calculateFrameProgress() {
|
|
7154
|
+
return calculateProgress(this.confirmedFrameNumber, this.framesCount);
|
|
6866
7155
|
}
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
this.resumeAfterDrain = true;
|
|
6870
|
-
this.loop.stop();
|
|
7156
|
+
calculateSampleProgress() {
|
|
7157
|
+
return calculateProgress(this.confirmedSampleNumber, this.samplesCount);
|
|
6871
7158
|
}
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
this.
|
|
6875
|
-
|
|
7159
|
+
updateOverallProgress() {
|
|
7160
|
+
this.frameProgress = this.calculateFrameProgress();
|
|
7161
|
+
if (isAudioEnabled(this.options)) this.sampleProgress = this.calculateSampleProgress();
|
|
7162
|
+
this.emit("PROGRESS", {
|
|
7163
|
+
frameProgress: this.frameProgress,
|
|
7164
|
+
sampleProgress: this.sampleProgress
|
|
7165
|
+
});
|
|
6876
7166
|
}
|
|
6877
|
-
|
|
6878
|
-
|
|
7167
|
+
updateFrameProgress(args) {
|
|
7168
|
+
this.confirmedFrameNumber = getConfirmedProgressNumber({
|
|
7169
|
+
args,
|
|
7170
|
+
kind: "frame",
|
|
7171
|
+
options: this.options
|
|
7172
|
+
});
|
|
7173
|
+
this.frameProgress = this.calculateFrameProgress();
|
|
7174
|
+
this.updateOverallProgress();
|
|
6879
7175
|
}
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
7176
|
+
updateSampleProgress(args) {
|
|
7177
|
+
this.confirmedSampleNumber = getConfirmedProgressNumber({
|
|
7178
|
+
args,
|
|
7179
|
+
kind: "sample",
|
|
7180
|
+
options: this.options
|
|
6884
7181
|
});
|
|
7182
|
+
this.sampleProgress = this.calculateSampleProgress();
|
|
7183
|
+
this.updateOverallProgress();
|
|
6885
7184
|
}
|
|
6886
|
-
|
|
6887
|
-
if (
|
|
6888
|
-
|
|
6889
|
-
|
|
7185
|
+
preview(args) {
|
|
7186
|
+
if (!args) throw error_createError({
|
|
7187
|
+
message: "Preview arguments are missing.",
|
|
7188
|
+
options: this.options
|
|
7189
|
+
});
|
|
7190
|
+
const hasAudio = this.samplesCount > 0;
|
|
6890
7191
|
this.mediaTransmissionStarted = false;
|
|
6891
|
-
this.
|
|
6892
|
-
this.
|
|
6893
|
-
this.
|
|
6894
|
-
this.
|
|
6895
|
-
this.
|
|
6896
|
-
this.
|
|
6897
|
-
this.
|
|
6898
|
-
this.
|
|
6899
|
-
this.
|
|
6900
|
-
this.
|
|
6901
|
-
|
|
6902
|
-
this.
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
7192
|
+
this.recordingActive = false;
|
|
7193
|
+
this.recordingTransportFailed = false;
|
|
7194
|
+
this.confirmedFrameNumber = this.confirmedSampleNumber = this.samplesCount = this.framesCount = 0;
|
|
7195
|
+
this.sampleProgress = this.frameProgress = void 0;
|
|
7196
|
+
this.key = args.key;
|
|
7197
|
+
if (args.mp4) this.replay.setMp4Source(`${args.mp4 + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.mp4`, true);
|
|
7198
|
+
if (args.webm) this.replay.setWebMSource(`${args.webm + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.webm`, true);
|
|
7199
|
+
this.hide();
|
|
7200
|
+
const widthDimension = this.getRecorderWidth(true);
|
|
7201
|
+
const heightDimension = this.getRecorderHeight(true);
|
|
7202
|
+
const duration = args.duration ?? -1;
|
|
7203
|
+
this.emit("PREVIEW", {
|
|
7204
|
+
key: this.key,
|
|
7205
|
+
width: widthDimension?.value,
|
|
7206
|
+
height: heightDimension?.value,
|
|
7207
|
+
hasAudio,
|
|
7208
|
+
duration
|
|
6907
7209
|
});
|
|
7210
|
+
if (this.stopTime) this.waitingTime = Date.now() - this.stopTime;
|
|
7211
|
+
this.recordingStats ??= {};
|
|
7212
|
+
this.recordingStats.waitingTime = this.waitingTime;
|
|
6908
7213
|
}
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
this.backpressureTimeout = window.setTimeout(()=>{
|
|
6912
|
-
this.backpressureTimeout = void 0;
|
|
6913
|
-
if (this.shouldIgnoreBackpressureTimeout()) return;
|
|
6914
|
-
const err = error_createError({
|
|
6915
|
-
message: "WebSocket write timed out",
|
|
6916
|
-
explanation: "The server stopped accepting recording data. The recording has been stopped; please check your connection and start a new one.",
|
|
6917
|
-
options: this.options
|
|
6918
|
-
});
|
|
6919
|
-
this.failMediaTransmission(err);
|
|
6920
|
-
}, this.options.timeouts.connection);
|
|
7214
|
+
cancelAnimationFrame() {
|
|
7215
|
+
this.loop?.dispose();
|
|
6921
7216
|
}
|
|
6922
|
-
|
|
6923
|
-
|
|
7217
|
+
getIntervalSum() {
|
|
7218
|
+
if (!this.loop) return;
|
|
7219
|
+
return this.loop.getElapsedTime();
|
|
6924
7220
|
}
|
|
6925
|
-
|
|
6926
|
-
|
|
7221
|
+
getAvgInterval() {
|
|
7222
|
+
const intervalSum = this.getIntervalSum() ?? 0;
|
|
7223
|
+
if (!this.hasValidInterval(intervalSum)) return;
|
|
7224
|
+
return intervalSum / this.framesCount;
|
|
6927
7225
|
}
|
|
6928
|
-
|
|
6929
|
-
const
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
message: "Recording interrupted",
|
|
6933
|
-
explanation: "The server connection closed before this recording completed. It cannot be safely resumed, so it has been stopped. Please start a new recording.",
|
|
6934
|
-
options: this.options,
|
|
6935
|
-
cause: {
|
|
6936
|
-
unconfirmedFrames,
|
|
6937
|
-
unconfirmedSamples
|
|
6938
|
-
}
|
|
6939
|
-
});
|
|
6940
|
-
this.failMediaTransmission(err);
|
|
7226
|
+
getAvgFps() {
|
|
7227
|
+
const intervalSum = this.getIntervalSum() ?? 0;
|
|
7228
|
+
if (!this.hasValidInterval(intervalSum)) return;
|
|
7229
|
+
return this.framesCount / intervalSum * 1000;
|
|
6941
7230
|
}
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
this.flushPendingWrites(stream);
|
|
7231
|
+
hasValidInterval(intervalSum) {
|
|
7232
|
+
return intervalSum > 0;
|
|
6945
7233
|
}
|
|
6946
|
-
|
|
6947
|
-
return
|
|
7234
|
+
getRecordingStats() {
|
|
7235
|
+
return this.recordingStats;
|
|
6948
7236
|
}
|
|
6949
|
-
|
|
6950
|
-
return
|
|
7237
|
+
getAudioSampleRate() {
|
|
7238
|
+
return this.recordingAudioSampleRate ?? this.userMedia?.getAudioSampleRate();
|
|
6951
7239
|
}
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
7240
|
+
stop(params) {
|
|
7241
|
+
this.cancelCameraRecovery();
|
|
7242
|
+
if (!this.userMedia) throw new Error("No user media defined, unable to stop");
|
|
7243
|
+
this.options.logger.debug(`Recorder: stop(${params ? string_pretty(params) : ""})`);
|
|
7244
|
+
this.recordingActive = false;
|
|
7245
|
+
const limitReached = params?.limitReached;
|
|
7246
|
+
this.emit("STOPPING", {
|
|
7247
|
+
limitReached
|
|
6958
7248
|
});
|
|
6959
|
-
this.
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
7249
|
+
this.loop?.complete?.();
|
|
7250
|
+
this.stopTimeout = window.setTimeout(()=>{
|
|
7251
|
+
this.stopTimeout = void 0;
|
|
7252
|
+
this.stopTime = Date.now();
|
|
7253
|
+
const videoType = this.replay.getVideoType();
|
|
7254
|
+
if (!videoType) throw new Error("Unable to video record when no video type is defined.");
|
|
7255
|
+
this.recordingStats = {
|
|
7256
|
+
avgFps: this.getAvgFps(),
|
|
7257
|
+
wantedFps: this.options.video.fps,
|
|
7258
|
+
avgInterval: this.getAvgInterval(),
|
|
7259
|
+
wantedInterval: 1e3 / this.options.video.fps,
|
|
7260
|
+
intervalSum: this.getIntervalSum(),
|
|
7261
|
+
framesCount: this.framesCount,
|
|
7262
|
+
videoType
|
|
7263
|
+
};
|
|
7264
|
+
if (this.userMedia) this.recordingStats.videoTrackLabel = this.userMedia.getVideoTrackLabel();
|
|
7265
|
+
if (isAudioEnabled(this.options) && this.userMedia) {
|
|
7266
|
+
this.recordingStats.samplesCount = this.samplesCount;
|
|
7267
|
+
this.recordingStats.sampleRate = this.recordingAudioSampleRate ?? this.userMedia.getAudioSampleRate();
|
|
7268
|
+
}
|
|
7269
|
+
this.writeCommand("stop", this.recordingStats, ()=>{
|
|
7270
|
+
this.emit("STOPPED", {
|
|
7271
|
+
recordingStats: this.recordingStats
|
|
7272
|
+
});
|
|
6975
7273
|
});
|
|
6976
|
-
|
|
7274
|
+
this.reset();
|
|
7275
|
+
}, 60);
|
|
6977
7276
|
}
|
|
6978
|
-
|
|
6979
|
-
|
|
7277
|
+
back(cb) {
|
|
7278
|
+
this.emit("GOING_BACK");
|
|
7279
|
+
this.key = void 0;
|
|
7280
|
+
this.recordingDimensions = void 0;
|
|
7281
|
+
this.unloaded = false;
|
|
7282
|
+
this.show();
|
|
7283
|
+
this.writeCommand("back", void 0, cb);
|
|
6980
7284
|
}
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
this.
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
});
|
|
7001
|
-
if (this.mediaTransmissionStarted) return void this.failInterruptedRecording();
|
|
7002
|
-
this.emit("ERROR", {
|
|
7003
|
-
err: error_createError({
|
|
7004
|
-
message: "WebSocket stream error",
|
|
7005
|
-
explanation: `The WebSocket stream emitted an error event. Details: ${string_pretty(err)}`,
|
|
7006
|
-
options: this.options,
|
|
7007
|
-
exc: streamError
|
|
7008
|
-
})
|
|
7009
|
-
});
|
|
7285
|
+
reInitializeAudio() {
|
|
7286
|
+
this.options.logger.debug("Recorder: reInitializeAudio()");
|
|
7287
|
+
this.clearUserMediaTimeout();
|
|
7288
|
+
this.userMediaLoading = false;
|
|
7289
|
+
this.userMedia?.stop();
|
|
7290
|
+
this.recordingDimensions = void 0;
|
|
7291
|
+
this.userMediaLoaded = this.key = this.canvas = this.ctx = void 0;
|
|
7292
|
+
this.loadUserMedia();
|
|
7293
|
+
}
|
|
7294
|
+
reset() {
|
|
7295
|
+
this.cancelCameraRecovery();
|
|
7296
|
+
if (!this.unloaded) {
|
|
7297
|
+
this.options.logger.debug("Recorder: reset()");
|
|
7298
|
+
this.emit("RESETTING");
|
|
7299
|
+
this.cancelAnimationFrame();
|
|
7300
|
+
this.userMedia?.stop();
|
|
7301
|
+
this.replay.reset();
|
|
7302
|
+
this.userMediaLoaded = this.key = this.canvas = this.ctx = this.recordingBuffer = void 0;
|
|
7303
|
+
}
|
|
7010
7304
|
}
|
|
7011
|
-
|
|
7012
|
-
this.
|
|
7013
|
-
this.pingInterval = window.setInterval(()=>{
|
|
7014
|
-
this.options.logger.debug("Recorder: pinging...");
|
|
7015
|
-
this.writeStream(Buffer.from(""));
|
|
7016
|
-
}, this.options.timeouts.pingInterval);
|
|
7305
|
+
validate() {
|
|
7306
|
+
return Boolean(this.key) && void 0 === this.canvas;
|
|
7017
7307
|
}
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
this.
|
|
7021
|
-
|
|
7022
|
-
|
|
7308
|
+
pause(params) {
|
|
7309
|
+
this.cancelCameraRecovery();
|
|
7310
|
+
this.resumeAfterDrain = false;
|
|
7311
|
+
const loop = this.loop;
|
|
7312
|
+
if (!loop) throw new Error("No recording loop defined");
|
|
7313
|
+
if (params) this.options.logger.debug(`pause() at frame ${this.framesCount} with ${string_pretty(params)}`);
|
|
7314
|
+
else this.options.logger.debug(`pause() at frame ${this.framesCount}`);
|
|
7315
|
+
this.userMedia?.pause();
|
|
7316
|
+
loop.stop();
|
|
7317
|
+
this.emit("PAUSED");
|
|
7318
|
+
this.sendPings();
|
|
7023
7319
|
}
|
|
7024
|
-
|
|
7025
|
-
if (this.
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
const
|
|
7031
|
-
|
|
7320
|
+
resume() {
|
|
7321
|
+
if (this.cameraRecovery || !this.userMedia?.hasLiveStream()) {
|
|
7322
|
+
this.recoverCamera();
|
|
7323
|
+
this.resumeAfterCameraRecovery = Boolean(this.cameraRecovery);
|
|
7324
|
+
return;
|
|
7325
|
+
}
|
|
7326
|
+
const loop = this.loop;
|
|
7327
|
+
if (!loop) throw new Error("No recording loop defined");
|
|
7328
|
+
this.options.logger.debug(`Recorder: resume() with frame ${this.framesCount}`);
|
|
7329
|
+
this.stopPings();
|
|
7330
|
+
this.emit("RESUMING");
|
|
7331
|
+
this.userMedia.resume();
|
|
7332
|
+
if (this.streamBackpressured) this.resumeAfterDrain = true;
|
|
7333
|
+
else loop.start();
|
|
7032
7334
|
}
|
|
7033
|
-
|
|
7034
|
-
|
|
7335
|
+
onFlushed(opts) {
|
|
7336
|
+
const frameNumber = opts.frameNumber;
|
|
7337
|
+
if (1 === frameNumber) this.emit("FIRST_FRAME_SENT");
|
|
7035
7338
|
}
|
|
7036
|
-
|
|
7339
|
+
draw(_deltaTime, elapsedTime) {
|
|
7340
|
+
if (this.recordingTransportFailed) return;
|
|
7341
|
+
if (!this.userMedia) throw new Error("No user media defined, unable to draw on canvas");
|
|
7037
7342
|
try {
|
|
7038
|
-
this.
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
if (
|
|
7343
|
+
if (!this.isPaused() && this.stream && this.ctx) {
|
|
7344
|
+
if (0 === this.framesCount) this.emit("SENDING_FIRST_FRAME");
|
|
7345
|
+
this.framesCount++;
|
|
7346
|
+
const imageSource = this.userMedia.getRawVisuals();
|
|
7347
|
+
if (this.canvas && imageSource) this.ctx.drawImage(imageSource, 0, 0, this.canvas.width, this.canvas.height);
|
|
7348
|
+
else throw new Error("Unable to draw an image without a defined canvas");
|
|
7349
|
+
this.recordingBuffer = this.frame?.toBuffer();
|
|
7350
|
+
const recordingBufferLength = this.recordingBuffer?.length;
|
|
7351
|
+
if (recordingBufferLength) {
|
|
7352
|
+
if (this.recordingBuffer) {
|
|
7353
|
+
const frameControlBuffer = capture_Buffer.from(JSON.stringify({
|
|
7354
|
+
frameNumber: this.framesCount
|
|
7355
|
+
}));
|
|
7356
|
+
const frameBuffer = capture_Buffer.concat([
|
|
7357
|
+
this.recordingBuffer,
|
|
7358
|
+
frameControlBuffer
|
|
7359
|
+
]);
|
|
7360
|
+
this.mediaTransmissionStarted = true;
|
|
7361
|
+
this.writeStream(frameBuffer, {
|
|
7362
|
+
frameNumber: this.framesCount,
|
|
7363
|
+
onFlushedCallback: this.onFlushed.bind(this)
|
|
7364
|
+
});
|
|
7365
|
+
this.visuals.checkTimer(elapsedTime);
|
|
7366
|
+
}
|
|
7367
|
+
} else throw error_createError({
|
|
7368
|
+
message: "Failed to extract webcam data.",
|
|
7369
|
+
options: this.options
|
|
7370
|
+
});
|
|
7043
7371
|
}
|
|
7044
|
-
this.userMediaLoading = this.blocking = this.unloaded = this.submitting = false;
|
|
7045
|
-
this.userMediaLoaded = true;
|
|
7046
|
-
this.clearUserMediaTimeout();
|
|
7047
|
-
if (!switchingFacingMode && !params?.recovering) this.loop = this.createLoop();
|
|
7048
|
-
this.show();
|
|
7049
|
-
if (params?.recordWhenReady) this.record();
|
|
7050
|
-
this.emit("USER_MEDIA_READY", {
|
|
7051
|
-
switchingFacingMode: params?.switchingFacingMode,
|
|
7052
|
-
recovering: params?.recovering,
|
|
7053
|
-
paused: this.isPaused(),
|
|
7054
|
-
recordWhenReady: params?.recordWhenReady
|
|
7055
|
-
});
|
|
7056
|
-
if (params?.recovering) this.finishCameraRecovery(true);
|
|
7057
7372
|
} catch (exc) {
|
|
7058
|
-
|
|
7059
|
-
else this.emit("ERROR", {
|
|
7373
|
+
this.emit("ERROR", {
|
|
7060
7374
|
exc
|
|
7061
7375
|
});
|
|
7062
7376
|
}
|
|
7063
7377
|
}
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
this.
|
|
7378
|
+
createLoop() {
|
|
7379
|
+
const newLoop = animitter({
|
|
7380
|
+
fps: this.options.video.fps
|
|
7381
|
+
}, this.draw.bind(this));
|
|
7382
|
+
this.originalAnimationFrameObject = newLoop.getRequestAnimationFrameObject();
|
|
7383
|
+
return newLoop;
|
|
7069
7384
|
}
|
|
7070
|
-
|
|
7071
|
-
if (
|
|
7072
|
-
this.
|
|
7073
|
-
|
|
7074
|
-
|
|
7385
|
+
record() {
|
|
7386
|
+
if (this.unloaded || this.cameraRecovery) return;
|
|
7387
|
+
if (!this.connected) {
|
|
7388
|
+
this.options.logger.debug("Recorder: reconnecting before recording …");
|
|
7389
|
+
this.initSocket(()=>{
|
|
7390
|
+
this.once("SERVER_READY", ()=>{
|
|
7391
|
+
if (this.unloaded) return;
|
|
7392
|
+
if (this.userMediaLoaded || this.options.loadUserMediaOnRecord) this.record();
|
|
7393
|
+
else this.once("USER_MEDIA_READY", ()=>{
|
|
7394
|
+
if (!this.unloaded) this.record();
|
|
7395
|
+
});
|
|
7396
|
+
});
|
|
7397
|
+
});
|
|
7398
|
+
return;
|
|
7399
|
+
}
|
|
7400
|
+
if (!this.userMediaLoaded) {
|
|
7401
|
+
if (this.options.loadUserMediaOnRecord) this.loadUserMedia({
|
|
7402
|
+
recordWhenReady: true
|
|
7403
|
+
});
|
|
7404
|
+
else {
|
|
7405
|
+
const err = error_createError({
|
|
7406
|
+
message: "Load and enable your camera first",
|
|
7407
|
+
options: this.options
|
|
7408
|
+
});
|
|
7409
|
+
this.emit("ERROR", {
|
|
7410
|
+
err
|
|
7411
|
+
});
|
|
7412
|
+
}
|
|
7413
|
+
return;
|
|
7414
|
+
}
|
|
7415
|
+
const loop = this.loop;
|
|
7416
|
+
if (!loop) throw new Error("No recording loop defined");
|
|
7417
|
+
this.recordingDimensions = void 0;
|
|
7418
|
+
try {
|
|
7419
|
+
if (!this.userMedia) throw new Error("No user media defined, unable to create canvas");
|
|
7420
|
+
this.canvas = this.userMedia.createCanvas();
|
|
7421
|
+
} catch (exc) {
|
|
7422
|
+
const err = error_createError({
|
|
7423
|
+
exc,
|
|
7424
|
+
options: this.options
|
|
7425
|
+
});
|
|
7426
|
+
this.emit("ERROR", {
|
|
7427
|
+
err
|
|
7428
|
+
});
|
|
7429
|
+
return;
|
|
7430
|
+
}
|
|
7431
|
+
this.ctx = this.canvas.getContext("2d");
|
|
7432
|
+
if (!this.canvas.width) {
|
|
7433
|
+
const err = error_createError({
|
|
7434
|
+
message: "Canvas has an invalid width.",
|
|
7435
|
+
options: this.options
|
|
7436
|
+
});
|
|
7437
|
+
this.emit("ERROR", {
|
|
7438
|
+
err
|
|
7439
|
+
});
|
|
7440
|
+
return;
|
|
7441
|
+
}
|
|
7442
|
+
if (!this.canvas.height) {
|
|
7443
|
+
const err = error_createError({
|
|
7444
|
+
message: "Canvas has an invalid height.",
|
|
7445
|
+
options: this.options
|
|
7446
|
+
});
|
|
7447
|
+
this.emit("ERROR", {
|
|
7448
|
+
err
|
|
7449
|
+
});
|
|
7450
|
+
return;
|
|
7451
|
+
}
|
|
7452
|
+
this.recordingDimensions = {
|
|
7453
|
+
width: this.canvas.width,
|
|
7454
|
+
height: this.canvas.height
|
|
7455
|
+
};
|
|
7456
|
+
this.frame = new canvas_to_buffer(this.canvas, this.options.image.types, this.options.image.quality);
|
|
7457
|
+
this.options.logger.debug("Recorder: record()");
|
|
7458
|
+
this.recordingActive = true;
|
|
7459
|
+
this.recordingTransportFailed = false;
|
|
7460
|
+
this.mediaTransmissionStarted = false;
|
|
7461
|
+
this.recordingAudioSampleRate = void 0;
|
|
7462
|
+
this.userMedia.record();
|
|
7463
|
+
this.emit("RECORDING", {
|
|
7464
|
+
framesCount: this.framesCount
|
|
7465
|
+
});
|
|
7466
|
+
loop.on("update", (_deltaTime, elapsedTime)=>{
|
|
7467
|
+
let avgFPS;
|
|
7468
|
+
avgFPS = 0 !== elapsedTime ? Math.round(this.framesCount / elapsedTime * 1000) : void 0;
|
|
7469
|
+
this.options.logger.debug(`Recorder updates avgFps = ${avgFPS} at frame ${this.framesCount}`);
|
|
7470
|
+
});
|
|
7471
|
+
loop.start();
|
|
7075
7472
|
}
|
|
7076
|
-
|
|
7077
|
-
if (
|
|
7078
|
-
|
|
7079
|
-
|
|
7473
|
+
setAnimationFrameObject(newObj) {
|
|
7474
|
+
if (this.loop) {
|
|
7475
|
+
const isRecording = this.isRecording();
|
|
7476
|
+
this.loop.stop();
|
|
7477
|
+
this.loop.setRequestAnimationFrameObject(newObj);
|
|
7478
|
+
if (isRecording) this.loop.start();
|
|
7479
|
+
}
|
|
7080
7480
|
}
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
this.
|
|
7084
|
-
window.clearTimeout(this.reconnectTimeout);
|
|
7085
|
-
this.reconnectTimeout = void 0;
|
|
7481
|
+
restoreAnimationFrameObject() {
|
|
7482
|
+
this.options.logger.debug("Recorder: restoreAnimationFrameObject()");
|
|
7483
|
+
if (this.originalAnimationFrameObject) this.setAnimationFrameObject(this.originalAnimationFrameObject);
|
|
7086
7484
|
}
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
this.
|
|
7485
|
+
loopWithTimeouts() {
|
|
7486
|
+
this.options.logger.debug("Recorder: loopWithTimeouts()");
|
|
7487
|
+
const wantedInterval = 1e3 / this.options.video.fps;
|
|
7488
|
+
let processingTime = 0;
|
|
7489
|
+
const raf = (fn)=>window.setTimeout(()=>{
|
|
7490
|
+
const start = Date.now();
|
|
7491
|
+
fn();
|
|
7492
|
+
processingTime = Date.now() - start;
|
|
7493
|
+
}, wantedInterval - processingTime);
|
|
7494
|
+
const cancel = (id)=>{
|
|
7495
|
+
window.clearTimeout(id);
|
|
7496
|
+
};
|
|
7497
|
+
this.setAnimationFrameObject({
|
|
7498
|
+
requestAnimationFrame: raf,
|
|
7499
|
+
cancelAnimationFrame: cancel
|
|
7500
|
+
});
|
|
7090
7501
|
}
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
this.
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
this.
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
if (this.shouldContinueReconnect()) this.initSocket();
|
|
7103
|
-
}, delayMs);
|
|
7104
|
-
return true;
|
|
7502
|
+
isPaused() {
|
|
7503
|
+
return this.userMedia?.isPaused() && !this.loop?.isRunning();
|
|
7504
|
+
}
|
|
7505
|
+
isUserMediaPaused() {
|
|
7506
|
+
return this.userMedia?.isPaused() ?? false;
|
|
7507
|
+
}
|
|
7508
|
+
isCaptureLoopRunning() {
|
|
7509
|
+
return this.loop?.isRunning() ?? false;
|
|
7510
|
+
}
|
|
7511
|
+
stopCaptureLoop() {
|
|
7512
|
+
this.loop?.stop();
|
|
7105
7513
|
}
|
|
7106
|
-
|
|
7107
|
-
|
|
7514
|
+
startCaptureLoop() {
|
|
7515
|
+
this.loop?.start();
|
|
7108
7516
|
}
|
|
7109
|
-
|
|
7110
|
-
return this.
|
|
7517
|
+
isRecording() {
|
|
7518
|
+
return Boolean(this.loop?.isRunning() && !this.isPaused() && !this.isNotifying() && this.stream && !this.stream.destroyed);
|
|
7111
7519
|
}
|
|
7112
|
-
|
|
7113
|
-
this.
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
this.streamBackpressured = false;
|
|
7117
|
-
this.resumeAfterDrain = false;
|
|
7118
|
-
const stream = this.stream;
|
|
7119
|
-
this.stream = void 0;
|
|
7120
|
-
if (stream && !stream.destroyed) stream.destroy();
|
|
7520
|
+
getRecordingDimensions() {
|
|
7521
|
+
return this.recordingDimensions && {
|
|
7522
|
+
...this.recordingDimensions
|
|
7523
|
+
};
|
|
7121
7524
|
}
|
|
7122
|
-
|
|
7123
|
-
|
|
7525
|
+
constructor(visuals, replay, options){
|
|
7526
|
+
super(visuals, options), capture_define_property(this, "replay", void 0), capture_define_property(this, "loop", void 0), capture_define_property(this, "originalAnimationFrameObject", void 0), capture_define_property(this, "recordingAudioSampleRate", void 0), capture_define_property(this, "frameProgress", void 0), capture_define_property(this, "sampleProgress", void 0), capture_define_property(this, "canvas", void 0), capture_define_property(this, "ctx", void 0), capture_define_property(this, "stopTime", void 0), capture_define_property(this, "waitingTime", void 0), capture_define_property(this, "frame", void 0), capture_define_property(this, "recordingBuffer", void 0);
|
|
7527
|
+
this.replay = replay;
|
|
7124
7528
|
}
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7529
|
+
}
|
|
7530
|
+
const capture = RecorderCapture;
|
|
7531
|
+
function figureMinHeight(height, options) {
|
|
7532
|
+
let minHeight;
|
|
7533
|
+
if (options.video.height) {
|
|
7534
|
+
minHeight = height ? Math.min(options.video.height, height) : options.video.height;
|
|
7535
|
+
if (minHeight < 1) throw error_createError({
|
|
7536
|
+
message: `Got a min height less than 1 (${minHeight})!`,
|
|
7537
|
+
options
|
|
7538
|
+
});
|
|
7539
|
+
} else minHeight = height;
|
|
7540
|
+
return minHeight;
|
|
7541
|
+
}
|
|
7542
|
+
const dimensions_figureMinHeight = figureMinHeight;
|
|
7543
|
+
function getRatio(options, videoHeight, videoWidth) {
|
|
7544
|
+
let ratio = 1;
|
|
7545
|
+
const hasVideoDimensions = videoHeight && videoWidth;
|
|
7546
|
+
const desiredHeight = options.video.height;
|
|
7547
|
+
const desiredWidth = options.video.width;
|
|
7548
|
+
const hasDesiredDimensions = desiredHeight && desiredWidth;
|
|
7549
|
+
if (hasDesiredDimensions) ratio = hasVideoDimensions ? videoHeight < desiredHeight || videoWidth < desiredWidth ? videoHeight / videoWidth : desiredHeight / desiredWidth : desiredHeight / desiredWidth;
|
|
7550
|
+
else if (hasVideoDimensions) ratio = videoHeight / videoWidth;
|
|
7551
|
+
return ratio;
|
|
7552
|
+
}
|
|
7553
|
+
const dimensions_getRatio = getRatio;
|
|
7554
|
+
function calculateHeight(responsive, videoWidth, options, ratio, element) {
|
|
7555
|
+
const dimension = {
|
|
7556
|
+
unit: "px"
|
|
7557
|
+
};
|
|
7558
|
+
let width = videoWidth;
|
|
7559
|
+
if (responsive && element) {
|
|
7560
|
+
const limitedDimension = dimensions_limitWidth(element, options, width);
|
|
7561
|
+
width = limitedDimension.value;
|
|
7562
|
+
}
|
|
7563
|
+
const chosenRatio = ratio ?? dimensions_getRatio(options, void 0, videoWidth);
|
|
7564
|
+
const height = width ? Math.round(width * chosenRatio) : void 0;
|
|
7565
|
+
if (height && Number.isInteger(height) && height < 1) throw error_createError({
|
|
7566
|
+
message: "Just calculated a height less than 1 which is wrong.",
|
|
7567
|
+
options
|
|
7568
|
+
});
|
|
7569
|
+
const minHeight = dimensions_figureMinHeight(height, options);
|
|
7570
|
+
dimension.value = minHeight;
|
|
7571
|
+
return dimension;
|
|
7572
|
+
}
|
|
7573
|
+
const dimensions_calculateHeight = calculateHeight;
|
|
7574
|
+
function calculateWidth(responsive, options, videoHeight, ratio) {
|
|
7575
|
+
const dimension = {
|
|
7576
|
+
unit: "px"
|
|
7577
|
+
};
|
|
7578
|
+
let height = dimensions_figureMinHeight(videoHeight, options);
|
|
7579
|
+
if (responsive) {
|
|
7580
|
+
const limitedDimension = dimensions_limitHeight(height, options, "calculateWidth");
|
|
7581
|
+
height = limitedDimension.value;
|
|
7582
|
+
}
|
|
7583
|
+
if (!height || height < 1) throw error_createError({
|
|
7584
|
+
message: `Height ${height} cannot be smaller than 1 when calculating width.`,
|
|
7585
|
+
options
|
|
7586
|
+
});
|
|
7587
|
+
const chosenRatio = ratio ?? dimensions_getRatio(options, videoHeight);
|
|
7588
|
+
const calculatedWidth = Math.round(height / chosenRatio);
|
|
7589
|
+
if (calculatedWidth < 1) throw error_createError({
|
|
7590
|
+
message: "Calculated width cannot be smaller than 1!",
|
|
7591
|
+
options
|
|
7592
|
+
});
|
|
7593
|
+
dimension.value = calculatedWidth;
|
|
7594
|
+
return dimension;
|
|
7595
|
+
}
|
|
7596
|
+
const dimensions_calculateWidth = calculateWidth;
|
|
7597
|
+
function dimensions_define_property(obj, key, value) {
|
|
7598
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
7599
|
+
value: value,
|
|
7600
|
+
enumerable: true,
|
|
7601
|
+
configurable: true,
|
|
7602
|
+
writable: true
|
|
7603
|
+
});
|
|
7604
|
+
else obj[key] = value;
|
|
7605
|
+
return obj;
|
|
7606
|
+
}
|
|
7607
|
+
class RecorderDimensions {
|
|
7608
|
+
correctDimensions() {
|
|
7609
|
+
const recorderElement = this.context.getRecorderElement();
|
|
7610
|
+
if (!recorderElement) return;
|
|
7611
|
+
const widthDimension = dimensions_useFullWidth(this.context.options.video.mobileBreakPoint);
|
|
7612
|
+
if (this.context.options.video.stretch || widthDimension) {
|
|
7613
|
+
recorderElement.style.width = "100%";
|
|
7614
|
+
recorderElement.style.removeProperty("height");
|
|
7615
|
+
} else {
|
|
7616
|
+
if (this.context.options.video.width) {
|
|
7617
|
+
const recorderWidth = this.getRecorderWidth(true);
|
|
7618
|
+
if (recorderWidth?.value) recorderElement.width = recorderWidth.value;
|
|
7619
|
+
else recorderElement.style.removeProperty("width");
|
|
7620
|
+
}
|
|
7621
|
+
if (this.context.options.video.height) {
|
|
7622
|
+
const recorderHeight = this.getRecorderHeight(true);
|
|
7623
|
+
if (recorderHeight?.value) recorderElement.height = recorderHeight.value;
|
|
7624
|
+
else recorderElement.style.removeProperty("height");
|
|
7625
|
+
}
|
|
7626
|
+
}
|
|
7627
|
+
}
|
|
7628
|
+
getRecorderWidth(responsive) {
|
|
7629
|
+
const userMedia = this.context.getUserMedia();
|
|
7630
|
+
if (userMedia?.hasVideoWidth()) return userMedia.getRawWidth(responsive);
|
|
7631
|
+
else if (responsive && this.context.options.video.width) return this.context.visuals.limitWidth(this.context.options.video.width);
|
|
7632
|
+
const dimension = {
|
|
7633
|
+
unit: "px",
|
|
7634
|
+
value: this.context.options.video.width
|
|
7155
7635
|
};
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7636
|
+
return dimension;
|
|
7637
|
+
}
|
|
7638
|
+
getRecorderHeight(responsive, useBoundingClientRect) {
|
|
7639
|
+
const recorderElement = this.context.getRecorderElement();
|
|
7640
|
+
const userMedia = this.context.getUserMedia();
|
|
7641
|
+
let recorderHeight;
|
|
7642
|
+
if (recorderElement && useBoundingClientRect) {
|
|
7643
|
+
const height = recorderElement.getBoundingClientRect().height;
|
|
7644
|
+
recorderHeight = {
|
|
7645
|
+
unit: "px",
|
|
7646
|
+
value: height
|
|
7647
|
+
};
|
|
7648
|
+
} else if (userMedia) {
|
|
7649
|
+
let height = userMedia.getRawHeight(responsive);
|
|
7650
|
+
if (void 0 !== height && this.context.options.video.width) {
|
|
7651
|
+
const ratio = this.getRatio();
|
|
7652
|
+
if (void 0 !== ratio) {
|
|
7653
|
+
const idealHeight = this.context.options.video.width * ratio;
|
|
7654
|
+
height = Math.min(idealHeight, height);
|
|
7655
|
+
}
|
|
7656
|
+
}
|
|
7657
|
+
recorderHeight = {
|
|
7658
|
+
unit: "px",
|
|
7659
|
+
value: height
|
|
7660
|
+
};
|
|
7661
|
+
} else if (responsive && this.context.options.video.height) recorderHeight = this.calculateHeight(responsive);
|
|
7662
|
+
else if (this.context.options.video.height) {
|
|
7663
|
+
const height = this.context.options.video.height;
|
|
7664
|
+
recorderHeight = {
|
|
7665
|
+
unit: "px",
|
|
7666
|
+
value: height
|
|
7667
|
+
};
|
|
7164
7668
|
}
|
|
7165
|
-
|
|
7166
|
-
this.discardSocket();
|
|
7167
|
-
const err = error_createError({
|
|
7168
|
-
message: "Unable to connect to the server",
|
|
7169
|
-
explanation,
|
|
7170
|
-
options: this.options,
|
|
7171
|
-
cause: contents,
|
|
7172
|
-
exc: socketError ? new Error("WebSocket connection error", {
|
|
7173
|
-
cause: socketError
|
|
7174
|
-
}) : void 0
|
|
7175
|
-
});
|
|
7176
|
-
this.emit("ERROR", {
|
|
7177
|
-
err
|
|
7178
|
-
});
|
|
7669
|
+
return recorderHeight;
|
|
7179
7670
|
}
|
|
7180
|
-
|
|
7181
|
-
|
|
7671
|
+
getRatio() {
|
|
7672
|
+
const userMedia = this.context.getUserMedia();
|
|
7673
|
+
let ratio;
|
|
7674
|
+
if (userMedia) {
|
|
7675
|
+
const userMediaVideoWidth = userMedia.getVideoWidth();
|
|
7676
|
+
const userMediaVideoHeight = userMedia.getVideoHeight();
|
|
7677
|
+
if (!userMediaVideoWidth || userMediaVideoWidth < 1) ratio = this.context.visuals.getRatio();
|
|
7678
|
+
else if (userMediaVideoHeight) ratio = userMediaVideoHeight / userMediaVideoWidth;
|
|
7679
|
+
} else ratio = dimensions_getRatio(this.context.options);
|
|
7680
|
+
return ratio;
|
|
7182
7681
|
}
|
|
7183
|
-
|
|
7184
|
-
|
|
7682
|
+
calculateWidth(responsive) {
|
|
7683
|
+
const userMedia = this.context.getUserMedia();
|
|
7684
|
+
const recorderElement = this.context.getRecorderElement();
|
|
7685
|
+
let videoHeight;
|
|
7686
|
+
if (userMedia) videoHeight = userMedia.getVideoHeight();
|
|
7687
|
+
else if (recorderElement) videoHeight = recorderElement.videoHeight || recorderElement.height;
|
|
7688
|
+
return dimensions_calculateWidth(responsive, this.context.options, videoHeight, this.getRatio());
|
|
7185
7689
|
}
|
|
7186
|
-
|
|
7187
|
-
|
|
7690
|
+
calculateHeight(responsive) {
|
|
7691
|
+
const userMedia = this.context.getUserMedia();
|
|
7692
|
+
const recorderElement = this.context.getRecorderElement();
|
|
7693
|
+
let videoDimension;
|
|
7694
|
+
if (userMedia) {
|
|
7695
|
+
const videoHeight = userMedia.getVideoHeight();
|
|
7696
|
+
videoDimension = {
|
|
7697
|
+
value: videoHeight,
|
|
7698
|
+
unit: "px"
|
|
7699
|
+
};
|
|
7700
|
+
} else if (recorderElement) {
|
|
7701
|
+
const videoHeight = recorderElement.videoHeight || recorderElement.height;
|
|
7702
|
+
videoDimension = {
|
|
7703
|
+
value: videoHeight,
|
|
7704
|
+
unit: "px"
|
|
7705
|
+
};
|
|
7706
|
+
} else videoDimension = dimensions_calculateHeight(responsive, void 0, this.context.options, this.getRatio(), recorderElement);
|
|
7707
|
+
return videoDimension;
|
|
7188
7708
|
}
|
|
7189
|
-
|
|
7190
|
-
this
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7709
|
+
constructor(context){
|
|
7710
|
+
dimensions_define_property(this, "context", void 0);
|
|
7711
|
+
this.context = context;
|
|
7712
|
+
}
|
|
7713
|
+
}
|
|
7714
|
+
const recorder_dimensions = RecorderDimensions;
|
|
7715
|
+
function getFirstVideoTrack(localMediaStream) {
|
|
7716
|
+
const videoTracks = localMediaStream.getVideoTracks();
|
|
7717
|
+
let videoTrack;
|
|
7718
|
+
if (videoTracks[0]) videoTrack = videoTracks[0];
|
|
7719
|
+
return videoTrack;
|
|
7720
|
+
}
|
|
7721
|
+
const media_getFirstVideoTrack = getFirstVideoTrack;
|
|
7722
|
+
const MEDIA_EVENTS = [
|
|
7723
|
+
"loadstart",
|
|
7724
|
+
"suspend",
|
|
7725
|
+
"progress",
|
|
7726
|
+
"abort",
|
|
7727
|
+
"emptied",
|
|
7728
|
+
"stalled",
|
|
7729
|
+
"pause",
|
|
7730
|
+
"loadeddata",
|
|
7731
|
+
"waiting",
|
|
7732
|
+
"playing",
|
|
7733
|
+
"canplay",
|
|
7734
|
+
"canplaythrough",
|
|
7735
|
+
"seeking",
|
|
7736
|
+
"seeked",
|
|
7737
|
+
"ended",
|
|
7738
|
+
"ratechange",
|
|
7739
|
+
"durationchange",
|
|
7740
|
+
"volumechange"
|
|
7741
|
+
];
|
|
7742
|
+
const mediaEvents = MEDIA_EVENTS;
|
|
7743
|
+
const AUDIO_RECORDER_PROCESSOR_NAME = "videomail-pcm-recorder";
|
|
7744
|
+
function registerAudioRecorderProcessor(name, Processor, register) {
|
|
7745
|
+
class PcmRecorderProcessor extends Processor {
|
|
7746
|
+
process(inputs) {
|
|
7747
|
+
if (this.recordingId < 0) return false;
|
|
7748
|
+
const channel = inputs[0]?.[0];
|
|
7749
|
+
if (!channel || 0 === this.recordingId) return true;
|
|
7750
|
+
let inputOffset = 0;
|
|
7751
|
+
while(inputOffset < channel.length){
|
|
7752
|
+
const count = Math.min(this.samples.length - this.offset, channel.length - inputOffset);
|
|
7753
|
+
this.samples.set(channel.subarray(inputOffset, inputOffset + count), this.offset);
|
|
7754
|
+
this.offset += count;
|
|
7755
|
+
inputOffset += count;
|
|
7756
|
+
if (this.offset === this.samples.length) {
|
|
7757
|
+
const samples = this.samples;
|
|
7758
|
+
this.port.postMessage({
|
|
7759
|
+
recordingId: this.recordingId,
|
|
7760
|
+
samples
|
|
7761
|
+
}, [
|
|
7762
|
+
samples.buffer
|
|
7763
|
+
]);
|
|
7764
|
+
this.samples = new Float32Array(this.offset);
|
|
7765
|
+
this.offset = 0;
|
|
7766
|
+
}
|
|
7767
|
+
}
|
|
7768
|
+
return true;
|
|
7769
|
+
}
|
|
7770
|
+
constructor(options){
|
|
7771
|
+
super();
|
|
7772
|
+
this.samples = new Float32Array(options.processorOptions.bufferSize);
|
|
7773
|
+
this.offset = 0;
|
|
7774
|
+
this.recordingId = 0;
|
|
7775
|
+
this.port.onmessage = (event)=>{
|
|
7776
|
+
const recordingId = event.data;
|
|
7777
|
+
if ("number" != typeof recordingId) return;
|
|
7778
|
+
if (!Number.isSafeInteger(recordingId) || recordingId < -1) return;
|
|
7779
|
+
this.recordingId = recordingId;
|
|
7780
|
+
this.offset = 0;
|
|
7781
|
+
};
|
|
7782
|
+
}
|
|
7783
|
+
}
|
|
7784
|
+
register(name, PcmRecorderProcessor);
|
|
7785
|
+
}
|
|
7786
|
+
function AudioRecorder_define_property(obj, key, value) {
|
|
7787
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
7788
|
+
value: value,
|
|
7789
|
+
enumerable: true,
|
|
7790
|
+
configurable: true,
|
|
7791
|
+
writable: true
|
|
7792
|
+
});
|
|
7793
|
+
else obj[key] = value;
|
|
7794
|
+
return obj;
|
|
7795
|
+
}
|
|
7796
|
+
const CHANNELS = 1;
|
|
7797
|
+
const DEFAULT_BUFFER_SIZE = 2048;
|
|
7798
|
+
const MIN_BUFFER_SIZE = 256;
|
|
7799
|
+
const MAX_BUFFER_SIZE = 16384;
|
|
7800
|
+
class AudioRecorder extends util_Despot {
|
|
7801
|
+
getAudioContext() {
|
|
7802
|
+
if (!this.vcAudioContext) this.vcAudioContext = new window.AudioContext();
|
|
7803
|
+
return this.vcAudioContext;
|
|
7196
7804
|
}
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
});
|
|
7203
|
-
} else throw error_createError({
|
|
7204
|
-
message: "Arguments are missing for updating the frame progress",
|
|
7805
|
+
getBufferSize() {
|
|
7806
|
+
const configuredSize = this.options.audio.bufferSize;
|
|
7807
|
+
const bufferSize = "auto" === configuredSize ? DEFAULT_BUFFER_SIZE : configuredSize;
|
|
7808
|
+
if (void 0 === bufferSize) throw error_createError({
|
|
7809
|
+
message: "Audio buffer size is required.",
|
|
7205
7810
|
options: this.options
|
|
7206
7811
|
});
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
}
|
|
7211
|
-
updateSampleProgress(args) {
|
|
7212
|
-
if (args) {
|
|
7213
|
-
if (!args.sample) throw error_createError({
|
|
7214
|
-
message: "The audio sample number is missing",
|
|
7215
|
-
options: this.options
|
|
7216
|
-
});
|
|
7217
|
-
} else throw error_createError({
|
|
7218
|
-
message: "Arguments are missing for updating the audio sample progress",
|
|
7812
|
+
const validBufferSize = Number.isInteger(bufferSize) && bufferSize >= MIN_BUFFER_SIZE && bufferSize <= MAX_BUFFER_SIZE && is_power_of_two(bufferSize);
|
|
7813
|
+
if (!validBufferSize) throw error_createError({
|
|
7814
|
+
message: `Audio buffer size must be a power of two between ${MIN_BUFFER_SIZE} and ${MAX_BUFFER_SIZE}. The current buffer size is ${bufferSize}.`,
|
|
7219
7815
|
options: this.options
|
|
7220
7816
|
});
|
|
7221
|
-
|
|
7222
|
-
this.sampleProgress = this.calculateSampleProgress();
|
|
7223
|
-
this.updateOverallProgress();
|
|
7817
|
+
return bufferSize;
|
|
7224
7818
|
}
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7819
|
+
validateVolume() {
|
|
7820
|
+
const { volume } = this.options.audio;
|
|
7821
|
+
const validVolume = Number.isFinite(volume) && volume >= 0 && volume <= 1;
|
|
7822
|
+
if (!validVolume) throw error_createError({
|
|
7823
|
+
message: `Audio volume must be between zero and one. The current volume is ${volume}.`,
|
|
7228
7824
|
options: this.options
|
|
7229
7825
|
});
|
|
7230
|
-
const hasAudio = this.samplesCount > 0;
|
|
7231
|
-
this.mediaTransmissionStarted = false;
|
|
7232
|
-
this.recordingActive = false;
|
|
7233
|
-
this.recordingTransportFailed = false;
|
|
7234
|
-
this.confirmedFrameNumber = this.confirmedSampleNumber = this.samplesCount = this.framesCount = 0;
|
|
7235
|
-
this.sampleProgress = this.frameProgress = void 0;
|
|
7236
|
-
this.key = args.key;
|
|
7237
|
-
if (args.mp4) this.replay.setMp4Source(`${args.mp4 + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.mp4`, true);
|
|
7238
|
-
if (args.webm) this.replay.setWebMSource(`${args.webm + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.webm`, true);
|
|
7239
|
-
this.hide();
|
|
7240
|
-
const widthDimension = this.getRecorderWidth(true);
|
|
7241
|
-
const heightDimension = this.getRecorderHeight(true);
|
|
7242
|
-
const duration = args.duration ?? -1;
|
|
7243
|
-
this.emit("PREVIEW", {
|
|
7244
|
-
key: this.key,
|
|
7245
|
-
width: widthDimension?.value,
|
|
7246
|
-
height: heightDimension?.value,
|
|
7247
|
-
hasAudio,
|
|
7248
|
-
duration
|
|
7249
|
-
});
|
|
7250
|
-
if (this.stopTime) this.waitingTime = Date.now() - this.stopTime;
|
|
7251
|
-
this.recordingStats ??= {};
|
|
7252
|
-
this.recordingStats.waitingTime = this.waitingTime;
|
|
7253
7826
|
}
|
|
7254
|
-
|
|
7255
|
-
if (this.
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
return;
|
|
7259
|
-
}
|
|
7260
|
-
if (this.connecting) return void this.options.logger.debug("Recorder: already **connecting**. Not going to initialize a new WebSocket connection.");
|
|
7261
|
-
this.clearReconnectTimeout();
|
|
7262
|
-
if (util_isAutomatedUserAgent()) {
|
|
7263
|
-
this.connecting = false;
|
|
7264
|
-
this.options.logger.debug("Recorder: skipping web socket connection for an automated crawler");
|
|
7265
|
-
return;
|
|
7266
|
-
}
|
|
7267
|
-
this.connecting = true;
|
|
7268
|
-
this.connectionFailed = false;
|
|
7269
|
-
this.connectingStartedAt = Date.now();
|
|
7270
|
-
this.lastCloseEvent = void 0;
|
|
7271
|
-
this.lastSocketError = void 0;
|
|
7272
|
-
this.emit("CONNECTING");
|
|
7273
|
-
let url2Connect;
|
|
7274
|
-
try {
|
|
7275
|
-
const socketUrlObj = new URL(this.options.socketUrl);
|
|
7276
|
-
socketUrlObj.searchParams.set(constants.WHITELIST_KEY_LABEL, this.options.whitelistKey);
|
|
7277
|
-
url2Connect = socketUrlObj.toString();
|
|
7278
|
-
} catch (exc) {
|
|
7279
|
-
this.connecting = this.connected = false;
|
|
7280
|
-
const err = error_createError({
|
|
7281
|
-
message: "Invalid WebSocket URL",
|
|
7282
|
-
explanation: `The configured socketUrl "${this.options.socketUrl}" is not a valid URL. Please check your videomail-client configuration.`,
|
|
7283
|
-
options: this.options,
|
|
7284
|
-
exc
|
|
7285
|
-
});
|
|
7286
|
-
this.emit("ERROR", {
|
|
7287
|
-
err
|
|
7288
|
-
});
|
|
7289
|
-
return;
|
|
7290
|
-
}
|
|
7291
|
-
this.options.logger.debug(`Recorder: initializing web socket stream to ${url2Connect}`);
|
|
7292
|
-
let nativeSocket;
|
|
7293
|
-
try {
|
|
7294
|
-
nativeSocket = new WebSocket(url2Connect);
|
|
7295
|
-
nativeSocket.addEventListener("close", (event)=>{
|
|
7296
|
-
this.lastCloseEvent = {
|
|
7297
|
-
code: event.code,
|
|
7298
|
-
reason: event.reason,
|
|
7299
|
-
wasClean: event.wasClean
|
|
7300
|
-
};
|
|
7301
|
-
});
|
|
7302
|
-
nativeSocket.addEventListener("error", (event)=>{
|
|
7303
|
-
this.lastSocketError = error_getEventDetails(event);
|
|
7304
|
-
});
|
|
7305
|
-
} catch (exc) {
|
|
7306
|
-
this.connecting = this.connected = false;
|
|
7307
|
-
const diagnostic = error_getWebSocketDiagnostic(url2Connect);
|
|
7308
|
-
const message = diagnostic.looksAutomated ? "Automated crawler: WebSocket not supported in this environment" : `Failed to construct WebSocket to ${url2Connect}`;
|
|
7309
|
-
const explanation = diagnostic.looksAutomated ? "Headless crawlers cannot use Videomail's WebSocket. No action needed." : `Please check your connection and try again. If the problem persists, contact us. Diagnostic: ${diagnostic.text}`;
|
|
7310
|
-
const err = error_createError({
|
|
7311
|
-
message,
|
|
7312
|
-
explanation,
|
|
7313
|
-
options: this.options,
|
|
7314
|
-
exc
|
|
7315
|
-
});
|
|
7316
|
-
this.emit("ERROR", {
|
|
7317
|
-
err
|
|
7318
|
-
});
|
|
7319
|
-
return;
|
|
7827
|
+
revokeModuleUrl() {
|
|
7828
|
+
if (this.moduleUrl) {
|
|
7829
|
+
URL.revokeObjectURL(this.moduleUrl);
|
|
7830
|
+
this.moduleUrl = void 0;
|
|
7320
7831
|
}
|
|
7832
|
+
}
|
|
7833
|
+
async loadWorklet(context) {
|
|
7834
|
+
const source = `(${registerAudioRecorderProcessor.toString()})(${JSON.stringify(AUDIO_RECORDER_PROCESSOR_NAME)}, AudioWorkletProcessor, registerProcessor);`;
|
|
7835
|
+
const moduleUrl = URL.createObjectURL(new Blob([
|
|
7836
|
+
source
|
|
7837
|
+
], {
|
|
7838
|
+
type: "text/javascript"
|
|
7839
|
+
}));
|
|
7840
|
+
this.moduleUrl = moduleUrl;
|
|
7321
7841
|
try {
|
|
7322
|
-
|
|
7323
|
-
}
|
|
7324
|
-
this.
|
|
7325
|
-
const err = error_createError({
|
|
7326
|
-
message: `Failed to create a stream to ${url2Connect}`,
|
|
7327
|
-
explanation: "Please check your connection and try again. If the problem persists, contact us.",
|
|
7328
|
-
options: this.options,
|
|
7329
|
-
exc
|
|
7330
|
-
});
|
|
7331
|
-
this.emit("ERROR", {
|
|
7332
|
-
err
|
|
7333
|
-
});
|
|
7334
|
-
}
|
|
7335
|
-
const stream = this.stream;
|
|
7336
|
-
if (stream) {
|
|
7337
|
-
const connectionTimeoutMs = this.options.timeouts.connection;
|
|
7338
|
-
this.connectionTimeout = window.setTimeout(()=>{
|
|
7339
|
-
if (this.isOnline() && this.isUserMediaLoaded()) {
|
|
7340
|
-
this.options.logger.debug(`${PIPE_SYMBOL}Reconnecting due to connection timeout.`);
|
|
7341
|
-
this.reconnecting = true;
|
|
7342
|
-
this.handleConnectionFailure({
|
|
7343
|
-
url2Connect,
|
|
7344
|
-
cause: "timeout"
|
|
7345
|
-
});
|
|
7346
|
-
} else this.handleConnectionFailure({
|
|
7347
|
-
url2Connect,
|
|
7348
|
-
cause: "timeout"
|
|
7349
|
-
});
|
|
7350
|
-
}, connectionTimeoutMs);
|
|
7351
|
-
stream.on("close", ()=>{
|
|
7352
|
-
this.handleStreamClose(url2Connect);
|
|
7353
|
-
});
|
|
7354
|
-
stream.on("connect", (args)=>{
|
|
7355
|
-
this.options.logger.debug(`${PIPE_SYMBOL}Stream *connect* event emitted with args: ${string_pretty(args)}`);
|
|
7356
|
-
this.clearConnectionTimeout();
|
|
7357
|
-
const isClosing = this.stream?.socket.readyState === WebSocket.CLOSING;
|
|
7358
|
-
if (!this.connected && !isClosing && !this.unloaded) {
|
|
7359
|
-
this.connected = true;
|
|
7360
|
-
this.connecting = this.unloaded = false;
|
|
7361
|
-
this.reconnecting = false;
|
|
7362
|
-
this.reconnectAttempts = 0;
|
|
7363
|
-
this.clearReconnectTimeout();
|
|
7364
|
-
this.emit("CONNECTED");
|
|
7365
|
-
cb?.();
|
|
7366
|
-
}
|
|
7367
|
-
});
|
|
7368
|
-
stream.on("data", (data)=>{
|
|
7369
|
-
this.options.logger.debug(`${PIPE_SYMBOL}Stream *data* event emitted`);
|
|
7370
|
-
try {
|
|
7371
|
-
const command = JSON.parse(data.toString());
|
|
7372
|
-
this.executeCommand(command);
|
|
7373
|
-
} catch (exc) {
|
|
7374
|
-
this.options.logger.error(`Failed to parse command: ${exc}`);
|
|
7375
|
-
const err = error_createError({
|
|
7376
|
-
message: "Invalid server command",
|
|
7377
|
-
explanation: `Contact us. The invalid command was: ${data.toString()}.`,
|
|
7378
|
-
options: this.options,
|
|
7379
|
-
exc
|
|
7380
|
-
});
|
|
7381
|
-
this.emit("ERROR", {
|
|
7382
|
-
err
|
|
7383
|
-
});
|
|
7384
|
-
}
|
|
7385
|
-
});
|
|
7386
|
-
stream.on("error", (err)=>{
|
|
7387
|
-
this.handleStreamError(url2Connect, err);
|
|
7388
|
-
});
|
|
7389
|
-
stream.on("drain", ()=>{
|
|
7390
|
-
this.handleStreamDrain(stream);
|
|
7391
|
-
});
|
|
7392
|
-
stream.on("preend", (args)=>{
|
|
7393
|
-
this.options.logger.debug(`${PIPE_SYMBOL}Stream *preend* event emitted with args: ${string_pretty(args)}`);
|
|
7394
|
-
});
|
|
7395
|
-
stream.on("end", ()=>{
|
|
7396
|
-
this.options.logger.debug(`${PIPE_SYMBOL}Stream *end* event emitted`);
|
|
7397
|
-
});
|
|
7398
|
-
stream.on("pipe", (src)=>{
|
|
7399
|
-
this.options.logger.debug(`${PIPE_SYMBOL}Stream *pipe* event emitted with src: ${string_pretty(src)}`);
|
|
7400
|
-
});
|
|
7401
|
-
stream.on("unpipe", (src)=>{
|
|
7402
|
-
this.options.logger.debug(`${PIPE_SYMBOL}Stream *unpipe* event emitted with src: ${string_pretty(src)}`);
|
|
7403
|
-
});
|
|
7404
|
-
stream.on("resume", (args)=>{
|
|
7405
|
-
this.options.logger.debug(`${PIPE_SYMBOL}Stream *resume* event emitted with args: ${string_pretty(args)}`);
|
|
7406
|
-
});
|
|
7407
|
-
stream.on("uncork", (args)=>{
|
|
7408
|
-
this.options.logger.debug(`${PIPE_SYMBOL}Stream *uncork* event emitted with args: ${string_pretty(args)}`);
|
|
7409
|
-
});
|
|
7410
|
-
stream.on("readable", (args)=>{
|
|
7411
|
-
this.options.logger.debug(`${PIPE_SYMBOL}Stream *readable* event emitted with args: ${string_pretty(args)}`);
|
|
7412
|
-
});
|
|
7413
|
-
stream.on("prefinish", (args)=>{
|
|
7414
|
-
this.options.logger.debug(`${PIPE_SYMBOL}Stream *prefinish* event emitted with args: ${string_pretty(args)}`);
|
|
7415
|
-
});
|
|
7416
|
-
stream.on("finish", (args)=>{
|
|
7417
|
-
this.options.logger.debug(`${PIPE_SYMBOL}Stream *finish* event emitted with args: ${string_pretty(args)}`);
|
|
7418
|
-
});
|
|
7842
|
+
await context.audioWorklet.addModule(moduleUrl);
|
|
7843
|
+
} finally{
|
|
7844
|
+
if (this.moduleUrl === moduleUrl) this.revokeModuleUrl();
|
|
7419
7845
|
}
|
|
7420
7846
|
}
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
if (!
|
|
7425
|
-
const
|
|
7426
|
-
if (
|
|
7427
|
-
|
|
7847
|
+
onAudioProcess(event) {
|
|
7848
|
+
const callback = this.audioCallback;
|
|
7849
|
+
if (!callback) return;
|
|
7850
|
+
if (!this.userMedia.isRecording() || this.userMedia.isPaused()) return;
|
|
7851
|
+
const message = event.data;
|
|
7852
|
+
if ("object" != typeof message || null === message) return;
|
|
7853
|
+
if (!("recordingId" in message) || message.recordingId !== this.recordingId) return;
|
|
7854
|
+
if (!("samples" in message) || !(message.samples instanceof Float32Array)) return;
|
|
7855
|
+
if (0 === message.samples.length) return;
|
|
7856
|
+
callback(new audio_sample(message.samples));
|
|
7428
7857
|
}
|
|
7429
|
-
|
|
7430
|
-
if (this.
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
if (errorListeners?.length) if (err.name !== error_VideomailError.MEDIA_DEVICE_NOT_SUPPORTED) {
|
|
7438
|
-
const videomailError = error_createError({
|
|
7439
|
-
err,
|
|
7440
|
-
options: this.options,
|
|
7441
|
-
usedConstraints
|
|
7442
|
-
});
|
|
7443
|
-
this.emit("ERROR", {
|
|
7444
|
-
err: videomailError
|
|
7445
|
-
});
|
|
7446
|
-
} else {
|
|
7447
|
-
this.options.logger.debug(`Recorder: ignore user media error ${string_pretty(err)}`);
|
|
7448
|
-
this.retryTimeout = window.setTimeout(()=>{
|
|
7449
|
-
this.retryTimeout = void 0;
|
|
7450
|
-
if (!this.unloaded) this.loadUserMedia();
|
|
7451
|
-
}, this.options.timeouts.userMedia);
|
|
7858
|
+
disconnect() {
|
|
7859
|
+
if (this.worklet) {
|
|
7860
|
+
this.worklet.port.onmessage = null;
|
|
7861
|
+
this.worklet.onprocessorerror = null;
|
|
7862
|
+
this.worklet.port.postMessage(-1);
|
|
7863
|
+
this.worklet.port.close();
|
|
7864
|
+
this.worklet.disconnect();
|
|
7865
|
+
this.worklet = void 0;
|
|
7452
7866
|
}
|
|
7453
|
-
|
|
7454
|
-
|
|
7867
|
+
this.audioInput?.disconnect();
|
|
7868
|
+
this.volume?.disconnect();
|
|
7869
|
+
this.audioInput = void 0;
|
|
7870
|
+
this.volume = void 0;
|
|
7871
|
+
this.audioCallback = void 0;
|
|
7872
|
+
this.recordingId = 0;
|
|
7873
|
+
}
|
|
7874
|
+
async init(localMediaStream) {
|
|
7875
|
+
this.options.logger.debug("AudioRecorder: init()");
|
|
7876
|
+
const initializationId = ++this.initializationId;
|
|
7877
|
+
this.disconnect();
|
|
7878
|
+
const bufferSize = this.getBufferSize();
|
|
7879
|
+
this.validateVolume();
|
|
7880
|
+
if ("function" != typeof window.AudioContext) throw error_createError({
|
|
7881
|
+
message: "Audio recording requires Web Audio support.",
|
|
7882
|
+
options: this.options
|
|
7883
|
+
});
|
|
7884
|
+
const context = this.getAudioContext();
|
|
7885
|
+
if (void 0 === context.audioWorklet || "function" != typeof window.AudioWorkletNode) throw error_createError({
|
|
7886
|
+
message: "Audio recording requires AudioWorklet support in a secure context.",
|
|
7887
|
+
options: this.options
|
|
7888
|
+
});
|
|
7889
|
+
const workletReady = this.workletReady ??= this.loadWorklet(context);
|
|
7890
|
+
try {
|
|
7891
|
+
await workletReady;
|
|
7892
|
+
} catch (exc) {
|
|
7893
|
+
if (this.workletReady === workletReady) this.workletReady = void 0;
|
|
7894
|
+
if (initializationId !== this.initializationId) return;
|
|
7455
7895
|
throw error_createError({
|
|
7456
|
-
|
|
7457
|
-
|
|
7896
|
+
message: "Unable to load the audio recorder worklet. Check AudioWorklet and Content Security Policy support for blob: scripts.",
|
|
7897
|
+
cause: exc,
|
|
7458
7898
|
options: this.options
|
|
7459
7899
|
});
|
|
7460
7900
|
}
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
track.stop();
|
|
7466
|
-
});
|
|
7901
|
+
if (initializationId !== this.initializationId) return;
|
|
7902
|
+
try {
|
|
7903
|
+
this.audioInput = context.createMediaStreamSource(localMediaStream);
|
|
7904
|
+
} catch (exc) {
|
|
7467
7905
|
throw error_createError({
|
|
7468
|
-
message: "
|
|
7906
|
+
message: "Webcam has no audio",
|
|
7907
|
+
exc,
|
|
7469
7908
|
options: this.options
|
|
7470
7909
|
});
|
|
7471
7910
|
}
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
this.
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7911
|
+
try {
|
|
7912
|
+
this.volume = context.createGain();
|
|
7913
|
+
this.volume.gain.value = this.options.audio.volume;
|
|
7914
|
+
this.worklet = new window.AudioWorkletNode(context, AUDIO_RECORDER_PROCESSOR_NAME, {
|
|
7915
|
+
channelCount: CHANNELS,
|
|
7916
|
+
channelCountMode: "explicit",
|
|
7917
|
+
outputChannelCount: [
|
|
7918
|
+
CHANNELS
|
|
7919
|
+
],
|
|
7920
|
+
processorOptions: {
|
|
7921
|
+
bufferSize
|
|
7922
|
+
}
|
|
7923
|
+
});
|
|
7924
|
+
this.worklet.port.onmessage = (event)=>{
|
|
7925
|
+
this.onAudioProcess(event);
|
|
7926
|
+
};
|
|
7927
|
+
this.worklet.onprocessorerror = ()=>{
|
|
7928
|
+
this.stop();
|
|
7929
|
+
this.emit("ERROR", {
|
|
7930
|
+
err: error_createError({
|
|
7931
|
+
message: "Audio recording processor failed.",
|
|
7932
|
+
options: this.options
|
|
7933
|
+
})
|
|
7479
7934
|
});
|
|
7480
|
-
}
|
|
7935
|
+
};
|
|
7936
|
+
this.audioInput.connect(this.volume);
|
|
7937
|
+
this.volume.connect(this.worklet);
|
|
7938
|
+
this.worklet.connect(context.destination);
|
|
7481
7939
|
} catch (exc) {
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
this.userMediaErrorCallback(exc, {
|
|
7488
|
-
audio: isAudioEnabled(this.options)
|
|
7940
|
+
this.disconnect();
|
|
7941
|
+
throw error_createError({
|
|
7942
|
+
message: "Failed to connect the audio recorder.",
|
|
7943
|
+
exc,
|
|
7944
|
+
options: this.options
|
|
7489
7945
|
});
|
|
7490
7946
|
}
|
|
7491
|
-
|
|
7492
|
-
|
|
7947
|
+
}
|
|
7948
|
+
record(cb) {
|
|
7949
|
+
this.options.logger.debug("AudioRecorder: record()");
|
|
7950
|
+
this.audioCallback = cb;
|
|
7951
|
+
this.updateRecordingState();
|
|
7952
|
+
}
|
|
7953
|
+
updateRecordingState() {
|
|
7954
|
+
if (!this.worklet) return;
|
|
7955
|
+
const shouldRecord = Boolean(this.audioCallback) && this.userMedia.isRecording() && !this.userMedia.isPaused();
|
|
7956
|
+
this.recordingId = shouldRecord ? ++this.recordingSequence : 0;
|
|
7957
|
+
this.worklet.port.postMessage(this.recordingId);
|
|
7958
|
+
}
|
|
7959
|
+
async resume() {
|
|
7960
|
+
const context = this.vcAudioContext;
|
|
7961
|
+
if (!context) return;
|
|
7962
|
+
if ("running" !== context.state) await context.resume();
|
|
7963
|
+
}
|
|
7964
|
+
stop() {
|
|
7965
|
+
this.options.logger.debug("AudioRecorder: stop()");
|
|
7966
|
+
this.initializationId++;
|
|
7967
|
+
this.disconnect();
|
|
7968
|
+
this.revokeModuleUrl();
|
|
7969
|
+
const context = this.vcAudioContext;
|
|
7970
|
+
this.vcAudioContext = void 0;
|
|
7971
|
+
this.workletReady = void 0;
|
|
7972
|
+
if (context && "closed" !== context.state) context.close().then(()=>{
|
|
7973
|
+
this.options.logger.debug("AudioRecorder: audio context is closed");
|
|
7974
|
+
}).catch((err)=>{
|
|
7975
|
+
if (err instanceof Error) return void this.options.logger.error(error_createError({
|
|
7976
|
+
err,
|
|
7977
|
+
options: this.options
|
|
7978
|
+
}));
|
|
7979
|
+
this.options.logger.error(err);
|
|
7980
|
+
});
|
|
7981
|
+
}
|
|
7982
|
+
getSampleRate() {
|
|
7983
|
+
if ("function" == typeof window.AudioContext) return this.getAudioContext().sampleRate;
|
|
7984
|
+
return -1;
|
|
7985
|
+
}
|
|
7986
|
+
constructor(userMedia, options){
|
|
7987
|
+
super("AudioRecorder", options), AudioRecorder_define_property(this, "userMedia", void 0), AudioRecorder_define_property(this, "worklet", void 0), AudioRecorder_define_property(this, "audioInput", void 0), AudioRecorder_define_property(this, "volume", void 0), AudioRecorder_define_property(this, "vcAudioContext", void 0), AudioRecorder_define_property(this, "workletReady", void 0), AudioRecorder_define_property(this, "moduleUrl", void 0), AudioRecorder_define_property(this, "audioCallback", void 0), AudioRecorder_define_property(this, "initializationId", 0), AudioRecorder_define_property(this, "recordingSequence", 0), AudioRecorder_define_property(this, "recordingId", 0);
|
|
7988
|
+
this.userMedia = userMedia;
|
|
7989
|
+
}
|
|
7990
|
+
}
|
|
7991
|
+
const media_AudioRecorder = AudioRecorder;
|
|
7992
|
+
const VIRTUAL_KEYWORDS = [
|
|
7993
|
+
"obs",
|
|
7994
|
+
"virtual",
|
|
7995
|
+
"manycam",
|
|
7996
|
+
"vcam",
|
|
7997
|
+
"splitcam",
|
|
7998
|
+
"droidcam"
|
|
7999
|
+
];
|
|
8000
|
+
function isVirtualCamera(videoTrack) {
|
|
8001
|
+
const capabilities = videoTrack.getCapabilities();
|
|
8002
|
+
if (!capabilities.frameRate) return true;
|
|
8003
|
+
const isVirtual = VIRTUAL_KEYWORDS.some((keyword)=>videoTrack.label.toLowerCase().includes(keyword));
|
|
8004
|
+
return isVirtual;
|
|
8005
|
+
}
|
|
8006
|
+
const util_isVirtualCamera = isVirtualCamera;
|
|
8007
|
+
function userMedia_define_property(obj, key, value) {
|
|
8008
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
8009
|
+
value: value,
|
|
8010
|
+
enumerable: true,
|
|
8011
|
+
configurable: true,
|
|
8012
|
+
writable: true
|
|
8013
|
+
});
|
|
8014
|
+
else obj[key] = value;
|
|
8015
|
+
return obj;
|
|
8016
|
+
}
|
|
8017
|
+
const EVENT_ASCII = "|—O—|";
|
|
8018
|
+
class UserMedia extends util_Despot {
|
|
8019
|
+
attachMediaStream(stream) {
|
|
8020
|
+
if (this.rawVisualUserMedia) {
|
|
8021
|
+
this.rawVisualUserMedia.srcObject = stream;
|
|
8022
|
+
const previousStream = this.currentVisualStream;
|
|
8023
|
+
this.currentVisualStream = stream;
|
|
8024
|
+
if (previousStream && previousStream !== stream) previousStream.getTracks().forEach((track)=>{
|
|
7493
8025
|
track.stop();
|
|
7494
8026
|
});
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
this
|
|
8027
|
+
} else throw error_createError({
|
|
8028
|
+
message: "Error attaching stream to element.",
|
|
8029
|
+
explanation: "Contact the developer about this",
|
|
8030
|
+
options: this.options
|
|
8031
|
+
});
|
|
8032
|
+
}
|
|
8033
|
+
setVisualStream(localMediaStream) {
|
|
8034
|
+
if (localMediaStream) this.attachMediaStream(localMediaStream);
|
|
8035
|
+
else {
|
|
8036
|
+
if (this.rawVisualUserMedia) {
|
|
8037
|
+
this.rawVisualUserMedia.srcObject = null;
|
|
8038
|
+
this.rawVisualUserMedia.removeAttribute("src");
|
|
8039
|
+
}
|
|
8040
|
+
this.currentVisualStream = void 0;
|
|
7498
8041
|
}
|
|
7499
8042
|
}
|
|
7500
|
-
|
|
7501
|
-
this.
|
|
7502
|
-
this.
|
|
7503
|
-
this.
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
this.
|
|
7516
|
-
const
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
8043
|
+
async restorePlayback() {
|
|
8044
|
+
if (!this.rawVisualUserMedia) throw new Error("No camera preview is available.");
|
|
8045
|
+
await this.rawVisualUserMedia.play();
|
|
8046
|
+
await this.audioRecorder?.resume();
|
|
8047
|
+
}
|
|
8048
|
+
hasLiveStream() {
|
|
8049
|
+
const videoReady = this.currentVisualStream?.getVideoTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
|
|
8050
|
+
const audioReady = !isAudioEnabled(this.options) || this.currentVisualStream?.getAudioTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
|
|
8051
|
+
return Boolean(this.currentVisualStream?.active && videoReady && audioReady);
|
|
8052
|
+
}
|
|
8053
|
+
hasEnded() {
|
|
8054
|
+
if (this.rawVisualUserMedia?.ended) return this.rawVisualUserMedia.ended;
|
|
8055
|
+
return !this.currentVisualStream?.active;
|
|
8056
|
+
}
|
|
8057
|
+
hasInvalidDimensions() {
|
|
8058
|
+
if (!this.rawVisualUserMedia) return false;
|
|
8059
|
+
const { videoWidth, videoHeight } = this.rawVisualUserMedia;
|
|
8060
|
+
return videoWidth > 0 && videoWidth < 3 || videoHeight > 0 && videoHeight < 3;
|
|
8061
|
+
}
|
|
8062
|
+
logEvent(eventType, params) {
|
|
8063
|
+
this.options.logger.debug(`UserMedia: ... ${EVENT_ASCII} event ${eventType}: ${string_pretty(params)}`);
|
|
8064
|
+
}
|
|
8065
|
+
unloadRemainingEventListeners() {
|
|
8066
|
+
this.options.logger.debug("UserMedia: unloadRemainingEventListeners()");
|
|
8067
|
+
this.clearInitializationListeners?.();
|
|
8068
|
+
mediaEvents.forEach((eventName)=>{
|
|
8069
|
+
this.rawVisualUserMedia?.removeEventListener(eventName, this.outputEvent);
|
|
7524
8070
|
});
|
|
8071
|
+
this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
|
|
8072
|
+
}
|
|
8073
|
+
audioRecord(audioCallback) {
|
|
8074
|
+
this.removeListener("SENDING_FIRST_FRAME");
|
|
8075
|
+
this.audioRecorder?.record(audioCallback);
|
|
7525
8076
|
}
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
8077
|
+
init(localMediaStream, videoCallback, audioCallback, endedEarlyCallback, params) {
|
|
8078
|
+
this.clearInitializationListeners?.();
|
|
8079
|
+
this.stop(localMediaStream, {
|
|
8080
|
+
aboutToInitialize: true,
|
|
8081
|
+
preserveRecordingState: Boolean(params?.switchingFacingMode || params?.recovering)
|
|
8082
|
+
});
|
|
8083
|
+
const initializationId = ++this.initializationId;
|
|
8084
|
+
this.onPlayReached = false;
|
|
8085
|
+
this.onLoadedMetaDataReached = false;
|
|
8086
|
+
this.playingPromiseReached = false;
|
|
8087
|
+
this.videoTrackLabel = void 0;
|
|
8088
|
+
if (isAudioEnabled(this.options)) this.audioRecorder ??= new media_AudioRecorder(this, this.options);
|
|
8089
|
+
const unloadAllEventListeners = ()=>{
|
|
8090
|
+
this.options.logger.debug("UserMedia: unloadAllEventListeners()");
|
|
8091
|
+
this.unloadRemainingEventListeners();
|
|
8092
|
+
this.removeListener("SENDING_FIRST_FRAME");
|
|
8093
|
+
this.rawVisualUserMedia?.removeEventListener("play", onPlay);
|
|
8094
|
+
this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
|
|
8095
|
+
};
|
|
8096
|
+
this.clearInitializationListeners = ()=>{
|
|
8097
|
+
this.rawVisualUserMedia?.removeEventListener("play", onPlay);
|
|
8098
|
+
this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
|
|
8099
|
+
this.clearInitializationListeners = void 0;
|
|
8100
|
+
};
|
|
8101
|
+
const play = ()=>{
|
|
8102
|
+
try {
|
|
8103
|
+
this.rawVisualUserMedia?.load();
|
|
8104
|
+
if (this.rawVisualUserMedia?.paused) {
|
|
8105
|
+
this.options.logger.debug(`UserMedia: play(): media.readyState=${this.rawVisualUserMedia.readyState}, media.paused=${this.rawVisualUserMedia.paused}, media.ended=${this.rawVisualUserMedia.ended}, media.played=${string_pretty(this.rawVisualUserMedia.played)}`);
|
|
8106
|
+
this.rawVisualUserMedia.play().then(()=>{
|
|
8107
|
+
if (initializationId !== this.initializationId) return;
|
|
8108
|
+
if (!this.playingPromiseReached) {
|
|
8109
|
+
this.options.logger.debug("UserMedia: play promise successful. Playing now.");
|
|
8110
|
+
this.playingPromiseReached = true;
|
|
8111
|
+
}
|
|
8112
|
+
}).catch((exc)=>{
|
|
8113
|
+
if (initializationId !== this.initializationId) return;
|
|
8114
|
+
if (exc instanceof Error && !params?.recovering) this.options.logger.warn(`Caught pending user media promise exception: ${exc.toString()}`);
|
|
8115
|
+
else {
|
|
8116
|
+
unloadAllEventListeners();
|
|
8117
|
+
endedEarlyCallback(error_createError({
|
|
8118
|
+
message: "Failed to play user media upon play event.",
|
|
8119
|
+
exc,
|
|
8120
|
+
options: this.options
|
|
8121
|
+
}));
|
|
8122
|
+
}
|
|
8123
|
+
});
|
|
8124
|
+
}
|
|
8125
|
+
} catch (exc) {
|
|
8126
|
+
unloadAllEventListeners();
|
|
8127
|
+
endedEarlyCallback(exc);
|
|
8128
|
+
}
|
|
8129
|
+
};
|
|
8130
|
+
const fireCallbacks = ()=>{
|
|
8131
|
+
const readyState = this.rawVisualUserMedia?.readyState;
|
|
8132
|
+
this.options.logger.debug(`UserMedia: fireCallbacks(readyState=${readyState}, onPlayReached=${this.onPlayReached}, onLoadedMetaDataReached=${this.onLoadedMetaDataReached})`);
|
|
8133
|
+
if (this.onPlayReached && this.onLoadedMetaDataReached) if (this.audioRecorder) {
|
|
8134
|
+
const initializeAudio = async (audioRecorder)=>{
|
|
8135
|
+
try {
|
|
8136
|
+
await audioRecorder.init(localMediaStream);
|
|
8137
|
+
if (initializationId !== this.initializationId || this.currentVisualStream !== localMediaStream) return;
|
|
8138
|
+
const onAudioReady = ()=>{
|
|
8139
|
+
if (initializationId !== this.initializationId || this.currentVisualStream !== localMediaStream) return;
|
|
8140
|
+
if (this.recording) this.audioRecord(audioCallback);
|
|
8141
|
+
else this.on("SENDING_FIRST_FRAME", ()=>{
|
|
8142
|
+
this.audioRecord(audioCallback);
|
|
8143
|
+
});
|
|
8144
|
+
this.clearInitializationListeners?.();
|
|
8145
|
+
videoCallback();
|
|
8146
|
+
};
|
|
8147
|
+
if (params?.recovering) await audioRecorder.resume();
|
|
8148
|
+
onAudioReady();
|
|
8149
|
+
} catch (exc) {
|
|
8150
|
+
if (initializationId !== this.initializationId) return;
|
|
8151
|
+
unloadAllEventListeners();
|
|
8152
|
+
this.stop(void 0, {
|
|
8153
|
+
preserveRecordingState: Boolean(params?.recovering)
|
|
8154
|
+
});
|
|
8155
|
+
endedEarlyCallback(exc);
|
|
8156
|
+
}
|
|
8157
|
+
};
|
|
8158
|
+
initializeAudio(this.audioRecorder);
|
|
8159
|
+
} else {
|
|
8160
|
+
this.clearInitializationListeners?.();
|
|
8161
|
+
videoCallback();
|
|
8162
|
+
}
|
|
8163
|
+
};
|
|
8164
|
+
const onPlay = ()=>{
|
|
8165
|
+
try {
|
|
8166
|
+
this.logEvent("play", {
|
|
8167
|
+
readyState: this.rawVisualUserMedia?.readyState,
|
|
8168
|
+
audio: isAudioEnabled(this.options),
|
|
8169
|
+
width: this.rawVisualUserMedia?.width,
|
|
8170
|
+
height: this.rawVisualUserMedia?.height,
|
|
8171
|
+
videoWidth: this.rawVisualUserMedia?.videoWidth,
|
|
8172
|
+
videoHeight: this.rawVisualUserMedia?.videoHeight
|
|
8173
|
+
});
|
|
8174
|
+
this.rawVisualUserMedia?.removeEventListener("play", onPlay);
|
|
8175
|
+
if (this.hasEnded() || this.hasInvalidDimensions()) endedEarlyCallback(error_createError({
|
|
8176
|
+
message: "Already busy",
|
|
8177
|
+
explanation: "Probably another browser window is using your webcam?",
|
|
8178
|
+
options: this.options
|
|
8179
|
+
}));
|
|
8180
|
+
else {
|
|
8181
|
+
this.onPlayReached = true;
|
|
8182
|
+
fireCallbacks();
|
|
8183
|
+
}
|
|
8184
|
+
} catch (exc) {
|
|
8185
|
+
unloadAllEventListeners();
|
|
8186
|
+
endedEarlyCallback(exc);
|
|
8187
|
+
}
|
|
7532
8188
|
};
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
8189
|
+
const onLoadedMetaData = ()=>{
|
|
8190
|
+
this.logEvent("loadedmetadata", {
|
|
8191
|
+
readyState: this.rawVisualUserMedia?.readyState,
|
|
8192
|
+
paused: this.rawVisualUserMedia?.paused,
|
|
8193
|
+
width: this.rawVisualUserMedia?.width,
|
|
8194
|
+
height: this.rawVisualUserMedia?.height,
|
|
8195
|
+
videoWidth: this.rawVisualUserMedia?.videoWidth,
|
|
8196
|
+
videoHeight: this.rawVisualUserMedia?.videoHeight
|
|
8197
|
+
});
|
|
8198
|
+
this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
|
|
8199
|
+
if (!this.hasEnded() && !this.hasInvalidDimensions()) {
|
|
8200
|
+
this.emit("LOADED_META_DATA");
|
|
8201
|
+
this.onLoadedMetaDataReached = true;
|
|
8202
|
+
fireCallbacks();
|
|
8203
|
+
}
|
|
7541
8204
|
};
|
|
7542
|
-
}
|
|
7543
|
-
loadUserMedia(params) {
|
|
7544
|
-
if (this.userMediaLoaded) {
|
|
7545
|
-
this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already loaded");
|
|
7546
|
-
this.onUserMediaReady(params);
|
|
7547
|
-
return;
|
|
7548
|
-
} else if (this.userMediaLoading) return void this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already asking for permission");
|
|
7549
|
-
this.options.logger.debug(`Recorder: loadUserMedia(${params ? string_pretty(params) : ""})`);
|
|
7550
|
-
this.emit("LOADING_USER_MEDIA");
|
|
7551
8205
|
try {
|
|
7552
|
-
|
|
7553
|
-
this.
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
audio: isAudioEnabled(this.options)
|
|
7559
|
-
});
|
|
7560
|
-
}
|
|
7561
|
-
}, this.options.timeouts.userMedia);
|
|
7562
|
-
this.userMediaLoading = true;
|
|
7563
|
-
this.loadGenuineUserMedia(params);
|
|
7564
|
-
} catch (exc) {
|
|
7565
|
-
this.clearUserMediaTimeout();
|
|
7566
|
-
this.options.logger.debug("Recorder: failed to load genuine user media");
|
|
7567
|
-
this.userMediaLoading = false;
|
|
7568
|
-
if (this.cameraRecovery) return void this.failCameraRecovery(exc);
|
|
7569
|
-
const errorListeners = this.getListeners("ERROR");
|
|
7570
|
-
if (errorListeners?.length) this.emit("ERROR", {
|
|
7571
|
-
exc
|
|
8206
|
+
const videoTrack = media_getFirstVideoTrack(localMediaStream);
|
|
8207
|
+
this.videoTrackLabel = videoTrack?.label;
|
|
8208
|
+
if (videoTrack) if (videoTrack.enabled) if (util_isVirtualCamera(videoTrack)) throw error_createError({
|
|
8209
|
+
message: "Virtual cameras are not allowed.",
|
|
8210
|
+
explanation: `Please use a real camera, not "${videoTrack.label}".`,
|
|
8211
|
+
options: this.options
|
|
7572
8212
|
});
|
|
7573
8213
|
else {
|
|
7574
|
-
|
|
7575
|
-
|
|
8214
|
+
let description = "";
|
|
8215
|
+
if (videoTrack.label.length > 0) description = description.concat(videoTrack.label);
|
|
8216
|
+
description = description.concat(` with enabled=${videoTrack.enabled}, muted=${videoTrack.muted}, readyState=${videoTrack.readyState}`);
|
|
8217
|
+
this.options.logger.debug(`UserMedia: ${videoTrack.kind} detected. ${description}`);
|
|
7576
8218
|
}
|
|
8219
|
+
else throw error_createError({
|
|
8220
|
+
message: "Webcam is disabled",
|
|
8221
|
+
explanation: "The video track seems to be disabled. Enable it in your system.",
|
|
8222
|
+
options: this.options
|
|
8223
|
+
});
|
|
8224
|
+
else this.options.logger.debug("UserMedia: detected (but no video tracks exist");
|
|
8225
|
+
this.rawVisualUserMedia?.addEventListener("loadedmetadata", onLoadedMetaData);
|
|
8226
|
+
this.rawVisualUserMedia?.addEventListener("play", onPlay);
|
|
8227
|
+
this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
|
|
8228
|
+
this.rawVisualUserMedia?.addEventListener("error", this.handleMediaError);
|
|
8229
|
+
this.setVisualStream(localMediaStream);
|
|
8230
|
+
play();
|
|
8231
|
+
} catch (exc) {
|
|
8232
|
+
unloadAllEventListeners();
|
|
8233
|
+
endedEarlyCallback(exc);
|
|
7577
8234
|
}
|
|
7578
8235
|
}
|
|
7579
|
-
|
|
7580
|
-
|
|
8236
|
+
getVideoTrackLabel() {
|
|
8237
|
+
return this.videoTrackLabel;
|
|
8238
|
+
}
|
|
8239
|
+
isReady() {
|
|
8240
|
+
return Boolean(this.rawVisualUserMedia?.srcObject || this.rawVisualUserMedia?.currentSrc);
|
|
8241
|
+
}
|
|
8242
|
+
stop(visualStream, params) {
|
|
7581
8243
|
try {
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
this.
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
break;
|
|
7595
|
-
case "error":
|
|
7596
|
-
{
|
|
7597
|
-
let explanation = "(No explanation given)";
|
|
7598
|
-
if (command.args?.err?.message) explanation = command.args.err.message;
|
|
7599
|
-
const err = error_createError({
|
|
7600
|
-
message: "Websocket error from Videomail server",
|
|
7601
|
-
explanation,
|
|
7602
|
-
err: deserializeError(command.args?.err),
|
|
7603
|
-
options: this.options
|
|
7604
|
-
});
|
|
7605
|
-
this.emit("ERROR", {
|
|
7606
|
-
err
|
|
7607
|
-
});
|
|
7608
|
-
break;
|
|
7609
|
-
}
|
|
7610
|
-
case "confirmFrame":
|
|
7611
|
-
this.updateFrameProgress(command.args);
|
|
7612
|
-
break;
|
|
7613
|
-
case "confirmSample":
|
|
7614
|
-
this.updateSampleProgress(command.args);
|
|
7615
|
-
break;
|
|
7616
|
-
case "beginAudioEncoding":
|
|
7617
|
-
this.emit("BEGIN_AUDIO_ENCODING");
|
|
7618
|
-
break;
|
|
7619
|
-
case "beginVideoEncoding":
|
|
7620
|
-
this.emit("BEGIN_VIDEO_ENCODING");
|
|
7621
|
-
break;
|
|
7622
|
-
default:
|
|
7623
|
-
{
|
|
7624
|
-
const err = error_createError({
|
|
7625
|
-
message: `Unknown server command: ${command.command}`,
|
|
7626
|
-
options: this.options
|
|
7627
|
-
});
|
|
7628
|
-
this.emit("ERROR", {
|
|
7629
|
-
err
|
|
7630
|
-
});
|
|
7631
|
-
break;
|
|
7632
|
-
}
|
|
8244
|
+
let chosenStream = visualStream;
|
|
8245
|
+
if (!params?.aboutToInitialize) {
|
|
8246
|
+
this.initializationId++;
|
|
8247
|
+
this.clearInitializationListeners?.();
|
|
8248
|
+
chosenStream ??= this.currentVisualStream;
|
|
8249
|
+
const tracks = chosenStream?.getTracks();
|
|
8250
|
+
if (tracks) tracks.forEach((track)=>{
|
|
8251
|
+
track.stop();
|
|
8252
|
+
});
|
|
8253
|
+
this.setVisualStream();
|
|
8254
|
+
this.audioRecorder?.stop();
|
|
8255
|
+
this.audioRecorder = void 0;
|
|
7633
8256
|
}
|
|
8257
|
+
if (!params?.preserveRecordingState) this.paused = this.recording = false;
|
|
7634
8258
|
} catch (exc) {
|
|
7635
8259
|
this.emit("ERROR", {
|
|
7636
8260
|
exc
|
|
7637
8261
|
});
|
|
7638
8262
|
}
|
|
7639
8263
|
}
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
if (this.connected) {
|
|
7648
|
-
if (this.stream) {
|
|
7649
|
-
if (args) this.options.logger.debug(`$ ${command} with ${string_pretty(args)}`);
|
|
7650
|
-
else this.options.logger.debug(`$ ${command}`);
|
|
7651
|
-
const commandObj = {
|
|
7652
|
-
command,
|
|
7653
|
-
args
|
|
7654
|
-
};
|
|
7655
|
-
this.writeStream(Buffer.from(JSON.stringify(commandObj)), cb ? {
|
|
7656
|
-
onFlushedCallback: ()=>{
|
|
7657
|
-
queueMicrotask(cb);
|
|
7658
|
-
}
|
|
7659
|
-
} : void 0);
|
|
7660
|
-
}
|
|
7661
|
-
} else {
|
|
7662
|
-
this.options.logger.debug(`Reconnecting for the command ${command} …`);
|
|
7663
|
-
this.initSocket(()=>{
|
|
7664
|
-
this.writeCommand(command, args, cb);
|
|
7665
|
-
});
|
|
7666
|
-
}
|
|
7667
|
-
}
|
|
7668
|
-
cancelAnimationFrame() {
|
|
7669
|
-
this.loop?.dispose();
|
|
7670
|
-
}
|
|
7671
|
-
getIntervalSum() {
|
|
7672
|
-
if (!this.loop) return;
|
|
7673
|
-
return this.loop.getElapsedTime();
|
|
7674
|
-
}
|
|
7675
|
-
getAvgInterval() {
|
|
7676
|
-
const intervalSum = this.getIntervalSum() ?? 0;
|
|
7677
|
-
if (!this.hasValidInterval(intervalSum)) return;
|
|
7678
|
-
return intervalSum / this.framesCount;
|
|
7679
|
-
}
|
|
7680
|
-
getAvgFps() {
|
|
7681
|
-
const intervalSum = this.getIntervalSum() ?? 0;
|
|
7682
|
-
if (!this.hasValidInterval(intervalSum)) return;
|
|
7683
|
-
return this.framesCount / intervalSum * 1000;
|
|
7684
|
-
}
|
|
7685
|
-
hasValidInterval(intervalSum) {
|
|
7686
|
-
return intervalSum > 0;
|
|
7687
|
-
}
|
|
7688
|
-
getRecordingStats() {
|
|
7689
|
-
return this.recordingStats;
|
|
7690
|
-
}
|
|
7691
|
-
getAudioSampleRate() {
|
|
7692
|
-
return this.recordingAudioSampleRate ?? this.userMedia?.getAudioSampleRate();
|
|
7693
|
-
}
|
|
7694
|
-
stop(params) {
|
|
7695
|
-
this.cancelCameraRecovery();
|
|
7696
|
-
if (!this.userMedia) throw new Error("No user media defined, unable to stop");
|
|
7697
|
-
this.options.logger.debug(`Recorder: stop(${params ? string_pretty(params) : ""})`);
|
|
7698
|
-
this.recordingActive = false;
|
|
7699
|
-
const limitReached = params?.limitReached;
|
|
7700
|
-
this.emit("STOPPING", {
|
|
7701
|
-
limitReached
|
|
7702
|
-
});
|
|
7703
|
-
this.loop?.complete();
|
|
7704
|
-
this.stopTimeout = window.setTimeout(()=>{
|
|
7705
|
-
this.stopTimeout = void 0;
|
|
7706
|
-
this.stopTime = Date.now();
|
|
7707
|
-
const videoType = this.replay.getVideoType();
|
|
7708
|
-
if (!videoType) throw new Error("Unable to video record when no video type is defined.");
|
|
7709
|
-
this.recordingStats = {
|
|
7710
|
-
avgFps: this.getAvgFps(),
|
|
7711
|
-
wantedFps: this.options.video.fps,
|
|
7712
|
-
avgInterval: this.getAvgInterval(),
|
|
7713
|
-
wantedInterval: 1e3 / this.options.video.fps,
|
|
7714
|
-
intervalSum: this.getIntervalSum(),
|
|
7715
|
-
framesCount: this.framesCount,
|
|
7716
|
-
videoType
|
|
7717
|
-
};
|
|
7718
|
-
if (this.userMedia) this.recordingStats.videoTrackLabel = this.userMedia.getVideoTrackLabel();
|
|
7719
|
-
if (isAudioEnabled(this.options) && this.userMedia) {
|
|
7720
|
-
this.recordingStats.samplesCount = this.samplesCount;
|
|
7721
|
-
this.recordingStats.sampleRate = this.recordingAudioSampleRate ?? this.userMedia.getAudioSampleRate();
|
|
7722
|
-
}
|
|
7723
|
-
this.writeCommand("stop", this.recordingStats, ()=>{
|
|
7724
|
-
this.emit("STOPPED", {
|
|
7725
|
-
recordingStats: this.recordingStats
|
|
7726
|
-
});
|
|
7727
|
-
});
|
|
7728
|
-
this.reset();
|
|
7729
|
-
}, 60);
|
|
8264
|
+
createCanvas() {
|
|
8265
|
+
const canvas = document.createElement("canvas");
|
|
8266
|
+
const rawWidthDimension = this.getRawWidth(true);
|
|
8267
|
+
if (rawWidthDimension.value) canvas.width = rawWidthDimension.value;
|
|
8268
|
+
const rawHeight = this.getRawHeight(true);
|
|
8269
|
+
if (rawHeight) canvas.height = rawHeight;
|
|
8270
|
+
return canvas;
|
|
7730
8271
|
}
|
|
7731
|
-
|
|
7732
|
-
this.
|
|
7733
|
-
this.
|
|
7734
|
-
this.recordingDimensions = void 0;
|
|
7735
|
-
this.unloaded = false;
|
|
7736
|
-
this.show();
|
|
7737
|
-
this.writeCommand("back", void 0, cb);
|
|
8272
|
+
getVideoHeight() {
|
|
8273
|
+
if (!this.rawVisualUserMedia) return;
|
|
8274
|
+
return this.rawVisualUserMedia.videoHeight || this.rawVisualUserMedia.height;
|
|
7738
8275
|
}
|
|
7739
|
-
|
|
7740
|
-
this.
|
|
7741
|
-
this.
|
|
7742
|
-
this.userMediaLoading = false;
|
|
7743
|
-
this.userMedia?.stop();
|
|
7744
|
-
this.recordingDimensions = void 0;
|
|
7745
|
-
this.userMediaLoaded = this.key = this.canvas = this.ctx = void 0;
|
|
7746
|
-
this.loadUserMedia();
|
|
8276
|
+
getVideoWidth() {
|
|
8277
|
+
if (!this.rawVisualUserMedia) return;
|
|
8278
|
+
return this.rawVisualUserMedia.videoWidth || this.rawVisualUserMedia.width;
|
|
7747
8279
|
}
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
let cause;
|
|
7752
|
-
if (e) cause = e.type;
|
|
7753
|
-
const prettyCause = cause ? `, cause: ${cause}` : "";
|
|
7754
|
-
this.options.logger.debug(`Recorder: unload()${prettyCause}`);
|
|
7755
|
-
this.recordingActive = false;
|
|
7756
|
-
this.mediaTransmissionStarted = false;
|
|
7757
|
-
this.reset();
|
|
7758
|
-
this.recordingDimensions = void 0;
|
|
7759
|
-
this.clearUserMediaTimeout();
|
|
7760
|
-
this.clearConnectionTimeout();
|
|
7761
|
-
this.clearReconnectTimeout();
|
|
7762
|
-
this.clearRetryTimeout();
|
|
7763
|
-
this.clearStopTimeout();
|
|
7764
|
-
this.stopPings();
|
|
7765
|
-
this.userMediaLoading = false;
|
|
7766
|
-
this.connecting = false;
|
|
7767
|
-
this.reconnecting = false;
|
|
7768
|
-
this.reconnectAttempts = 0;
|
|
7769
|
-
if (this.userMedia) this.userMedia.unloadRemainingEventListeners();
|
|
7770
|
-
if (this.submitting) ;
|
|
7771
|
-
else if (this.stream) this.discardSocket();
|
|
7772
|
-
this.unloaded = true;
|
|
7773
|
-
this.built = this.connecting = this.connected = false;
|
|
8280
|
+
hasVideoWidth() {
|
|
8281
|
+
const videoWidth = this.getVideoWidth();
|
|
8282
|
+
return videoWidth && videoWidth > 0;
|
|
7774
8283
|
}
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
this.
|
|
7783
|
-
|
|
8284
|
+
getRawWidth(responsive) {
|
|
8285
|
+
let rawWidth;
|
|
8286
|
+
const widthDimension = {
|
|
8287
|
+
unit: "px"
|
|
8288
|
+
};
|
|
8289
|
+
rawWidth = responsive ? this.getVideoWidth() : this.options.video.width ? this.options.video.width : this.getVideoWidth();
|
|
8290
|
+
if (responsive) {
|
|
8291
|
+
const widthDimension = this.recorder.limitWidth(rawWidth);
|
|
8292
|
+
rawWidth = widthDimension?.value;
|
|
7784
8293
|
}
|
|
8294
|
+
widthDimension.value = rawWidth;
|
|
8295
|
+
return widthDimension;
|
|
7785
8296
|
}
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
8297
|
+
getRawHeight(responsive) {
|
|
8298
|
+
let rawHeight;
|
|
8299
|
+
if (responsive) {
|
|
8300
|
+
rawHeight = this.options.video.height || this.getVideoHeight();
|
|
8301
|
+
const ratio = this.recorder.getRatio();
|
|
8302
|
+
const rawWidth = this.getRawWidth(responsive);
|
|
8303
|
+
if (void 0 !== ratio && rawWidth.value) rawHeight = rawWidth.value * ratio;
|
|
8304
|
+
} else rawHeight = this.options.video.height ? this.options.video.height : this.getVideoHeight();
|
|
8305
|
+
if (responsive) {
|
|
8306
|
+
const heightDimension = this.recorder.limitHeight(rawHeight);
|
|
8307
|
+
rawHeight = heightDimension.value;
|
|
7791
8308
|
}
|
|
8309
|
+
return rawHeight;
|
|
7792
8310
|
}
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
window.clearTimeout(this.stopTimeout);
|
|
7796
|
-
this.stopTimeout = void 0;
|
|
7797
|
-
}
|
|
7798
|
-
validate() {
|
|
7799
|
-
return Boolean(this.key) && void 0 === this.canvas;
|
|
8311
|
+
getRawVisuals() {
|
|
8312
|
+
return this.rawVisualUserMedia;
|
|
7800
8313
|
}
|
|
7801
|
-
|
|
7802
|
-
|
|
8314
|
+
pause() {
|
|
8315
|
+
this.paused = true;
|
|
8316
|
+
this.audioRecorder?.updateRecordingState();
|
|
7803
8317
|
}
|
|
7804
|
-
|
|
7805
|
-
this.
|
|
7806
|
-
const resolve = this.resolveCameraRecovery;
|
|
7807
|
-
const resumeRequested = restored && this.resumeAfterCameraRecovery;
|
|
7808
|
-
this.cameraRecovery = void 0;
|
|
7809
|
-
this.resolveCameraRecovery = void 0;
|
|
7810
|
-
this.resumeAfterCameraRecovery = false;
|
|
7811
|
-
resolve?.(restored);
|
|
7812
|
-
if (resumeRequested && this.isPaused()) this.resume();
|
|
8318
|
+
isPaused() {
|
|
8319
|
+
return this.paused;
|
|
7813
8320
|
}
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
this.
|
|
7817
|
-
this.userMediaLoading = false;
|
|
7818
|
-
this.userMediaLoaded = false;
|
|
7819
|
-
this.clearUserMediaTimeout();
|
|
7820
|
-
this.userMedia?.stop(void 0, {
|
|
7821
|
-
preserveRecordingState: true
|
|
7822
|
-
});
|
|
7823
|
-
this.finishCameraRecovery(false);
|
|
8321
|
+
resume() {
|
|
8322
|
+
this.paused = false;
|
|
8323
|
+
this.audioRecorder?.updateRecordingState();
|
|
7824
8324
|
}
|
|
7825
|
-
|
|
7826
|
-
this.
|
|
7827
|
-
|
|
7828
|
-
message: "Camera recovery failed",
|
|
7829
|
-
explanation: "Your recording is still paused. Check camera permissions, then try Resume again or stop to keep the recorded part.",
|
|
7830
|
-
exc,
|
|
7831
|
-
options: this.options
|
|
7832
|
-
});
|
|
7833
|
-
this.emit("CAMERA_RECOVERY_FAILED", {
|
|
7834
|
-
err
|
|
7835
|
-
});
|
|
8325
|
+
record() {
|
|
8326
|
+
this.recording = true;
|
|
8327
|
+
this.audioRecorder?.updateRecordingState();
|
|
7836
8328
|
}
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
if (!this.built || this.unloaded || !this.userMedia || this.userMediaLoading || this.submitting || this.key || void 0 !== this.stopTimeout || this.recordingTransportFailed) return Promise.resolve(false);
|
|
7840
|
-
const liveStream = this.userMedia.hasLiveStream();
|
|
7841
|
-
if (liveStream && this.isRecording()) return Promise.resolve(true);
|
|
7842
|
-
if (!liveStream && this.recordingActive && !this.isPaused()) this.pause();
|
|
7843
|
-
const recovery = new Promise((resolve)=>{
|
|
7844
|
-
this.resolveCameraRecovery = resolve;
|
|
7845
|
-
});
|
|
7846
|
-
this.cameraRecovery = recovery;
|
|
7847
|
-
this.emit("CAMERA_RECOVERING");
|
|
7848
|
-
if (liveStream) {
|
|
7849
|
-
this.userMediaTimeout = window.setTimeout(()=>{
|
|
7850
|
-
this.failCameraRecovery(new Error("Camera playback recovery timed out."));
|
|
7851
|
-
}, this.options.timeouts.userMedia);
|
|
7852
|
-
this.userMedia.restorePlayback().then(()=>{
|
|
7853
|
-
if (this.cameraRecovery === recovery) this.finishCameraRecovery(true);
|
|
7854
|
-
}).catch((exc)=>{
|
|
7855
|
-
if (this.cameraRecovery === recovery) this.failCameraRecovery(exc);
|
|
7856
|
-
});
|
|
7857
|
-
} else {
|
|
7858
|
-
this.userMediaLoaded = false;
|
|
7859
|
-
this.loadUserMedia({
|
|
7860
|
-
recovering: true
|
|
7861
|
-
});
|
|
7862
|
-
}
|
|
7863
|
-
return recovery;
|
|
8329
|
+
isRecording() {
|
|
8330
|
+
return this.recording;
|
|
7864
8331
|
}
|
|
7865
|
-
|
|
7866
|
-
this.
|
|
7867
|
-
|
|
7868
|
-
if (params) this.options.logger.debug(`pause() at frame ${this.framesCount} with ${string_pretty(params)}`);
|
|
7869
|
-
else this.options.logger.debug(`pause() at frame ${this.framesCount}`);
|
|
7870
|
-
this.userMedia?.pause();
|
|
7871
|
-
this.loop.stop();
|
|
7872
|
-
this.emit("PAUSED");
|
|
7873
|
-
this.sendPings();
|
|
8332
|
+
getAudioSampleRate() {
|
|
8333
|
+
if (this.audioRecorder) return this.audioRecorder.getSampleRate();
|
|
8334
|
+
return -1;
|
|
7874
8335
|
}
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
this.
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
this.userMedia.resume();
|
|
7885
|
-
if (this.streamBackpressured) this.resumeAfterDrain = true;
|
|
7886
|
-
else this.loop.start();
|
|
8336
|
+
getCharacteristics() {
|
|
8337
|
+
return {
|
|
8338
|
+
audioSampleRate: this.getAudioSampleRate(),
|
|
8339
|
+
muted: this.rawVisualUserMedia?.muted,
|
|
8340
|
+
width: this.rawVisualUserMedia?.width,
|
|
8341
|
+
height: this.rawVisualUserMedia?.height,
|
|
8342
|
+
videoWidth: this.rawVisualUserMedia?.videoWidth,
|
|
8343
|
+
videoHeight: this.rawVisualUserMedia?.videoHeight
|
|
8344
|
+
};
|
|
7887
8345
|
}
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
8346
|
+
constructor(recorder, options){
|
|
8347
|
+
super("UserMedia", options), userMedia_define_property(this, "recorder", void 0), userMedia_define_property(this, "rawVisualUserMedia", void 0), userMedia_define_property(this, "paused", false), userMedia_define_property(this, "recording", false), userMedia_define_property(this, "audioRecorder", void 0), userMedia_define_property(this, "currentVisualStream", void 0), userMedia_define_property(this, "onPlayReached", false), userMedia_define_property(this, "onLoadedMetaDataReached", false), userMedia_define_property(this, "playingPromiseReached", false), userMedia_define_property(this, "videoTrackLabel", void 0), userMedia_define_property(this, "clearInitializationListeners", void 0), userMedia_define_property(this, "initializationId", 0), userMedia_define_property(this, "outputEvent", (e)=>{
|
|
8348
|
+
this.logEvent(e.type, {
|
|
8349
|
+
readyState: this.rawVisualUserMedia?.readyState
|
|
8350
|
+
});
|
|
8351
|
+
this.rawVisualUserMedia?.removeEventListener(e.type, this.outputEvent);
|
|
8352
|
+
}), userMedia_define_property(this, "handleMediaError", (event)=>{
|
|
8353
|
+
this.options.logger.warn(`Caught video element error event: ${string_pretty(event)}`);
|
|
8354
|
+
});
|
|
8355
|
+
this.recorder = recorder;
|
|
8356
|
+
this.rawVisualUserMedia = recorder.getRawVisualUserMedia();
|
|
8357
|
+
mediaEvents.forEach((eventName)=>{
|
|
8358
|
+
this.rawVisualUserMedia?.addEventListener(eventName, this.outputEvent, false);
|
|
8359
|
+
});
|
|
7891
8360
|
}
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
8361
|
+
}
|
|
8362
|
+
const visuals_userMedia = UserMedia;
|
|
8363
|
+
function recorder_define_property(obj, key, value) {
|
|
8364
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
8365
|
+
value: value,
|
|
8366
|
+
enumerable: true,
|
|
8367
|
+
configurable: true,
|
|
8368
|
+
writable: true
|
|
8369
|
+
});
|
|
8370
|
+
else obj[key] = value;
|
|
8371
|
+
return obj;
|
|
8372
|
+
}
|
|
8373
|
+
class Recorder extends capture {
|
|
8374
|
+
executeCommand(command) {
|
|
8375
|
+
if (this.unloaded) return;
|
|
7895
8376
|
try {
|
|
7896
|
-
if (
|
|
7897
|
-
|
|
7898
|
-
this.
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
8377
|
+
if (command.args) {
|
|
8378
|
+
const debugLine = string_summarize(`Server commanded: ${command.command} with:`, command.args);
|
|
8379
|
+
this.options.logger.debug(debugLine);
|
|
8380
|
+
} else this.options.logger.debug(`Server commanded: ${command.command}`);
|
|
8381
|
+
switch(command.command){
|
|
8382
|
+
case "ready":
|
|
8383
|
+
this.emit("SERVER_READY");
|
|
8384
|
+
if (!this.userMediaTimeout) if (this.options.loadUserMediaOnRecord) this.show();
|
|
8385
|
+
else this.loadUserMedia();
|
|
8386
|
+
break;
|
|
8387
|
+
case "preview":
|
|
8388
|
+
this.preview(command.args);
|
|
8389
|
+
break;
|
|
8390
|
+
case "error":
|
|
8391
|
+
{
|
|
8392
|
+
let explanation = "(No explanation given)";
|
|
8393
|
+
if (command.args?.err?.message) explanation = command.args.err.message;
|
|
8394
|
+
const err = error_createError({
|
|
8395
|
+
message: "Websocket error from Videomail server",
|
|
8396
|
+
explanation,
|
|
8397
|
+
err: deserializeError(command.args?.err),
|
|
8398
|
+
options: this.options
|
|
7917
8399
|
});
|
|
7918
|
-
this.
|
|
8400
|
+
this.emit("ERROR", {
|
|
8401
|
+
err
|
|
8402
|
+
});
|
|
8403
|
+
break;
|
|
8404
|
+
}
|
|
8405
|
+
case "confirmFrame":
|
|
8406
|
+
this.updateFrameProgress(command.args);
|
|
8407
|
+
break;
|
|
8408
|
+
case "confirmSample":
|
|
8409
|
+
this.updateSampleProgress(command.args);
|
|
8410
|
+
break;
|
|
8411
|
+
case "beginAudioEncoding":
|
|
8412
|
+
this.emit("BEGIN_AUDIO_ENCODING");
|
|
8413
|
+
break;
|
|
8414
|
+
case "beginVideoEncoding":
|
|
8415
|
+
this.emit("BEGIN_VIDEO_ENCODING");
|
|
8416
|
+
break;
|
|
8417
|
+
default:
|
|
8418
|
+
{
|
|
8419
|
+
const err = error_createError({
|
|
8420
|
+
message: `Unknown server command: ${command.command}`,
|
|
8421
|
+
options: this.options
|
|
8422
|
+
});
|
|
8423
|
+
this.emit("ERROR", {
|
|
8424
|
+
err
|
|
8425
|
+
});
|
|
8426
|
+
break;
|
|
7919
8427
|
}
|
|
7920
|
-
} else throw error_createError({
|
|
7921
|
-
message: "Failed to extract webcam data.",
|
|
7922
|
-
options: this.options
|
|
7923
|
-
});
|
|
7924
8428
|
}
|
|
7925
8429
|
} catch (exc) {
|
|
7926
8430
|
this.emit("ERROR", {
|
|
@@ -7928,156 +8432,34 @@ class Recorder extends util_Despot {
|
|
|
7928
8432
|
});
|
|
7929
8433
|
}
|
|
7930
8434
|
}
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
if (this.unloaded || this.cameraRecovery) return;
|
|
7940
|
-
if (!this.connected) {
|
|
7941
|
-
this.options.logger.debug("Recorder: reconnecting before recording …");
|
|
7942
|
-
this.initSocket(()=>{
|
|
7943
|
-
this.once("SERVER_READY", ()=>{
|
|
7944
|
-
if (this.unloaded) return;
|
|
7945
|
-
if (this.userMediaLoaded || this.options.loadUserMediaOnRecord) this.record();
|
|
7946
|
-
else this.once("USER_MEDIA_READY", ()=>{
|
|
7947
|
-
if (!this.unloaded) this.record();
|
|
7948
|
-
});
|
|
7949
|
-
});
|
|
7950
|
-
});
|
|
7951
|
-
return;
|
|
7952
|
-
}
|
|
7953
|
-
if (!this.userMediaLoaded) {
|
|
7954
|
-
if (this.options.loadUserMediaOnRecord) this.loadUserMedia({
|
|
7955
|
-
recordWhenReady: true
|
|
7956
|
-
});
|
|
7957
|
-
else {
|
|
7958
|
-
const err = error_createError({
|
|
7959
|
-
message: "Load and enable your camera first",
|
|
7960
|
-
options: this.options
|
|
7961
|
-
});
|
|
7962
|
-
this.emit("ERROR", {
|
|
7963
|
-
err
|
|
7964
|
-
});
|
|
7965
|
-
}
|
|
7966
|
-
return;
|
|
7967
|
-
}
|
|
7968
|
-
this.recordingDimensions = void 0;
|
|
7969
|
-
try {
|
|
7970
|
-
if (!this.userMedia) throw new Error("No user media defined, unable to create canvas");
|
|
7971
|
-
this.canvas = this.userMedia.createCanvas();
|
|
7972
|
-
} catch (exc) {
|
|
7973
|
-
const err = error_createError({
|
|
7974
|
-
exc,
|
|
7975
|
-
options: this.options
|
|
7976
|
-
});
|
|
7977
|
-
this.emit("ERROR", {
|
|
7978
|
-
err
|
|
7979
|
-
});
|
|
7980
|
-
return;
|
|
7981
|
-
}
|
|
7982
|
-
this.ctx = this.canvas.getContext("2d");
|
|
7983
|
-
if (!this.canvas.width) {
|
|
7984
|
-
const err = error_createError({
|
|
7985
|
-
message: "Canvas has an invalid width.",
|
|
7986
|
-
options: this.options
|
|
7987
|
-
});
|
|
7988
|
-
this.emit("ERROR", {
|
|
7989
|
-
err
|
|
7990
|
-
});
|
|
7991
|
-
return;
|
|
7992
|
-
}
|
|
7993
|
-
if (!this.canvas.height) {
|
|
7994
|
-
const err = error_createError({
|
|
7995
|
-
message: "Canvas has an invalid height.",
|
|
7996
|
-
options: this.options
|
|
7997
|
-
});
|
|
7998
|
-
this.emit("ERROR", {
|
|
7999
|
-
err
|
|
8000
|
-
});
|
|
8001
|
-
return;
|
|
8002
|
-
}
|
|
8003
|
-
this.recordingDimensions = {
|
|
8004
|
-
width: this.canvas.width,
|
|
8005
|
-
height: this.canvas.height
|
|
8006
|
-
};
|
|
8007
|
-
this.frame = new canvas_to_buffer(this.canvas, this.options.image.types, this.options.image.quality);
|
|
8008
|
-
this.options.logger.debug("Recorder: record()");
|
|
8009
|
-
this.recordingActive = true;
|
|
8010
|
-
this.recordingTransportFailed = false;
|
|
8435
|
+
unload(params) {
|
|
8436
|
+
if (this.unloaded || !this.built) return;
|
|
8437
|
+
const e = params?.e;
|
|
8438
|
+
let cause;
|
|
8439
|
+
if (e) cause = e.type;
|
|
8440
|
+
const prettyCause = cause ? `, cause: ${cause}` : "";
|
|
8441
|
+
this.options.logger.debug(`Recorder: unload()${prettyCause}`);
|
|
8442
|
+
this.recordingActive = false;
|
|
8011
8443
|
this.mediaTransmissionStarted = false;
|
|
8012
|
-
this.
|
|
8013
|
-
this.
|
|
8014
|
-
this.
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
this.
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
this.
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
this.loop.setRequestAnimationFrameObject(newObj);
|
|
8029
|
-
if (isRecording) this.loop.start();
|
|
8030
|
-
}
|
|
8031
|
-
}
|
|
8032
|
-
restoreAnimationFrameObject() {
|
|
8033
|
-
this.options.logger.debug("Recorder: restoreAnimationFrameObject()");
|
|
8034
|
-
this.setAnimationFrameObject(this.originalAnimationFrameObject);
|
|
8035
|
-
}
|
|
8036
|
-
loopWithTimeouts() {
|
|
8037
|
-
this.options.logger.debug("Recorder: loopWithTimeouts()");
|
|
8038
|
-
const wantedInterval = 1e3 / this.options.video.fps;
|
|
8039
|
-
let processingTime = 0;
|
|
8040
|
-
let start;
|
|
8041
|
-
const raf = (fn)=>setTimeout(()=>{
|
|
8042
|
-
start = Date.now();
|
|
8043
|
-
fn();
|
|
8044
|
-
processingTime = Date.now() - start;
|
|
8045
|
-
}, wantedInterval - processingTime);
|
|
8046
|
-
const cancel = (id)=>{
|
|
8047
|
-
window.clearTimeout(id);
|
|
8048
|
-
};
|
|
8049
|
-
this.setAnimationFrameObject({
|
|
8050
|
-
requestAnimationFrame: raf,
|
|
8051
|
-
cancelAnimationFrame: cancel
|
|
8052
|
-
});
|
|
8444
|
+
this.reset();
|
|
8445
|
+
this.recordingDimensions = void 0;
|
|
8446
|
+
this.clearUserMediaTimeout();
|
|
8447
|
+
this.clearConnectionTimeout();
|
|
8448
|
+
this.clearReconnectTimeout();
|
|
8449
|
+
this.clearRetryTimeout();
|
|
8450
|
+
this.clearStopTimeout();
|
|
8451
|
+
this.stopPings();
|
|
8452
|
+
this.userMediaLoading = false;
|
|
8453
|
+
this.connecting = false;
|
|
8454
|
+
this.reconnecting = false;
|
|
8455
|
+
this.reconnectAttempts = 0;
|
|
8456
|
+
if (this.userMedia) this.userMedia.unloadRemainingEventListeners();
|
|
8457
|
+
this.unloaded = true;
|
|
8458
|
+
this.discardSocket();
|
|
8459
|
+
this.built = this.connecting = this.connected = false;
|
|
8053
8460
|
}
|
|
8054
8461
|
correctDimensions() {
|
|
8055
|
-
|
|
8056
|
-
const widthDimension = dimensions_useFullWidth(this.options.video.mobileBreakPoint);
|
|
8057
|
-
if (this.options.video.stretch || widthDimension) {
|
|
8058
|
-
this.recorderElement.style.width = "100%";
|
|
8059
|
-
this.recorderElement.style.removeProperty("height");
|
|
8060
|
-
} else {
|
|
8061
|
-
if (this.options.video.width) {
|
|
8062
|
-
const recorderWidth = this.getRecorderWidth(true);
|
|
8063
|
-
if (recorderWidth?.value) this.recorderElement.width = recorderWidth.value;
|
|
8064
|
-
else this.recorderElement.style.removeProperty("width");
|
|
8065
|
-
}
|
|
8066
|
-
if (this.options.video.height) {
|
|
8067
|
-
const recorderHeight = this.getRecorderHeight(true);
|
|
8068
|
-
if (recorderHeight?.value) this.recorderElement.height = recorderHeight.value;
|
|
8069
|
-
else this.recorderElement.style.removeProperty("height");
|
|
8070
|
-
}
|
|
8071
|
-
}
|
|
8072
|
-
}
|
|
8073
|
-
switchFacingMode() {
|
|
8074
|
-
if (this.cameraRecovery || !util_getBrowser(this.options).isMobile()) return;
|
|
8075
|
-
if ("user" === this.facingMode) this.facingMode = "environment";
|
|
8076
|
-
else if ("environment" === this.facingMode) this.facingMode = "user";
|
|
8077
|
-
else this.options.logger.warn(`Recorder: unsupported facing mode ${string_pretty(this.facingMode)}`);
|
|
8078
|
-
this.loadGenuineUserMedia({
|
|
8079
|
-
switchingFacingMode: this.facingMode
|
|
8080
|
-
});
|
|
8462
|
+
this.dimensions.correctDimensions();
|
|
8081
8463
|
}
|
|
8082
8464
|
initEvents() {
|
|
8083
8465
|
this.options.logger.debug("Recorder: initEvents()");
|
|
@@ -8149,100 +8531,20 @@ class Recorder extends util_Despot {
|
|
|
8149
8531
|
}
|
|
8150
8532
|
this.built = true;
|
|
8151
8533
|
}
|
|
8152
|
-
isPaused() {
|
|
8153
|
-
return this.userMedia?.isPaused() && !this.loop.isRunning();
|
|
8154
|
-
}
|
|
8155
|
-
isRecording() {
|
|
8156
|
-
return Boolean(this.loop?.isRunning() && !this.isPaused() && !this.isNotifying() && this.stream && !this.stream.destroyed);
|
|
8157
|
-
}
|
|
8158
|
-
hide() {
|
|
8159
|
-
if (!this.isHidden()) {
|
|
8160
|
-
if (this.recorderElement) html_hideElement(this.recorderElement);
|
|
8161
|
-
this.clearUserMediaTimeout();
|
|
8162
|
-
this.clearRetryTimeout();
|
|
8163
|
-
}
|
|
8164
|
-
}
|
|
8165
|
-
isUnloaded() {
|
|
8166
|
-
return this.unloaded;
|
|
8167
|
-
}
|
|
8168
8534
|
getRecorderWidth(responsive) {
|
|
8169
|
-
|
|
8170
|
-
else if (responsive && this.options.video.width) return this.limitWidth(this.options.video.width);
|
|
8171
|
-
const dimension = {
|
|
8172
|
-
unit: "px",
|
|
8173
|
-
value: this.options.video.width
|
|
8174
|
-
};
|
|
8175
|
-
return dimension;
|
|
8176
|
-
}
|
|
8177
|
-
getRecordingDimensions() {
|
|
8178
|
-
return this.recordingDimensions && {
|
|
8179
|
-
...this.recordingDimensions
|
|
8180
|
-
};
|
|
8535
|
+
return this.dimensions.getRecorderWidth(responsive);
|
|
8181
8536
|
}
|
|
8182
8537
|
getRecorderHeight(responsive, useBoundingClientRect) {
|
|
8183
|
-
|
|
8184
|
-
if (this.recorderElement && useBoundingClientRect) {
|
|
8185
|
-
const height = this.recorderElement.getBoundingClientRect().height;
|
|
8186
|
-
recorderHeight = {
|
|
8187
|
-
unit: "px",
|
|
8188
|
-
value: height
|
|
8189
|
-
};
|
|
8190
|
-
} else if (this.userMedia) {
|
|
8191
|
-
let height = this.userMedia.getRawHeight(responsive);
|
|
8192
|
-
if (void 0 !== height && this.options.video.width) {
|
|
8193
|
-
const ratio = this.getRatio();
|
|
8194
|
-
if (void 0 !== ratio) {
|
|
8195
|
-
const idealHeight = this.options.video.width * ratio;
|
|
8196
|
-
height = Math.min(idealHeight, height);
|
|
8197
|
-
}
|
|
8198
|
-
}
|
|
8199
|
-
recorderHeight = {
|
|
8200
|
-
unit: "px",
|
|
8201
|
-
value: height
|
|
8202
|
-
};
|
|
8203
|
-
} else if (responsive && this.options.video.height) recorderHeight = this.calculateHeight(responsive);
|
|
8204
|
-
else if (this.options.video.height) {
|
|
8205
|
-
const height = this.options.video.height;
|
|
8206
|
-
recorderHeight = {
|
|
8207
|
-
unit: "px",
|
|
8208
|
-
value: height
|
|
8209
|
-
};
|
|
8210
|
-
}
|
|
8211
|
-
return recorderHeight;
|
|
8538
|
+
return this.dimensions.getRecorderHeight(responsive, useBoundingClientRect);
|
|
8212
8539
|
}
|
|
8213
8540
|
getRatio() {
|
|
8214
|
-
|
|
8215
|
-
if (this.userMedia) {
|
|
8216
|
-
const userMediaVideoWidth = this.userMedia.getVideoWidth();
|
|
8217
|
-
const userMediaVideoHeight = this.userMedia.getVideoHeight();
|
|
8218
|
-
if (!userMediaVideoWidth || userMediaVideoWidth < 1) ratio = this.visuals.getRatio();
|
|
8219
|
-
else if (userMediaVideoHeight) ratio = userMediaVideoHeight / userMediaVideoWidth;
|
|
8220
|
-
} else ratio = dimensions_getRatio(this.options);
|
|
8221
|
-
return ratio;
|
|
8541
|
+
return this.dimensions.getRatio();
|
|
8222
8542
|
}
|
|
8223
8543
|
calculateWidth(responsive) {
|
|
8224
|
-
|
|
8225
|
-
if (this.userMedia) videoHeight = this.userMedia.getVideoHeight();
|
|
8226
|
-
else if (this.recorderElement) videoHeight = this.recorderElement.videoHeight || this.recorderElement.height;
|
|
8227
|
-
const ratio = this.getRatio();
|
|
8228
|
-
return dimensions_calculateWidth(responsive, this.options, videoHeight, ratio);
|
|
8544
|
+
return this.dimensions.calculateWidth(responsive);
|
|
8229
8545
|
}
|
|
8230
8546
|
calculateHeight(responsive) {
|
|
8231
|
-
|
|
8232
|
-
if (this.userMedia) {
|
|
8233
|
-
const videoHeight = this.userMedia.getVideoHeight();
|
|
8234
|
-
videoDimension = {
|
|
8235
|
-
value: videoHeight,
|
|
8236
|
-
unit: "px"
|
|
8237
|
-
};
|
|
8238
|
-
} else if (this.recorderElement) {
|
|
8239
|
-
const videoHeight = this.recorderElement.videoHeight || this.recorderElement.height;
|
|
8240
|
-
videoDimension = {
|
|
8241
|
-
value: videoHeight,
|
|
8242
|
-
unit: "px"
|
|
8243
|
-
};
|
|
8244
|
-
} else videoDimension = dimensions_calculateHeight(responsive, void 0, this.options, this.getRatio(), this.recorderElement);
|
|
8245
|
-
return videoDimension;
|
|
8547
|
+
return this.dimensions.calculateHeight(responsive);
|
|
8246
8548
|
}
|
|
8247
8549
|
getRawVisualUserMedia() {
|
|
8248
8550
|
return this.recorderElement;
|
|
@@ -8259,14 +8561,14 @@ class Recorder extends util_Despot {
|
|
|
8259
8561
|
limitHeight(height) {
|
|
8260
8562
|
return this.visuals.limitHeight(height);
|
|
8261
8563
|
}
|
|
8262
|
-
isUserMediaLoaded() {
|
|
8263
|
-
return this.userMediaLoaded;
|
|
8264
|
-
}
|
|
8265
8564
|
constructor(visuals, replay, options){
|
|
8266
|
-
super(
|
|
8267
|
-
this.
|
|
8268
|
-
|
|
8269
|
-
|
|
8565
|
+
super(visuals, replay, options), recorder_define_property(this, "dimensions", void 0);
|
|
8566
|
+
this.dimensions = new recorder_dimensions({
|
|
8567
|
+
options,
|
|
8568
|
+
visuals,
|
|
8569
|
+
getRecorderElement: ()=>this.recorderElement,
|
|
8570
|
+
getUserMedia: ()=>this.userMedia
|
|
8571
|
+
});
|
|
8270
8572
|
}
|
|
8271
8573
|
}
|
|
8272
8574
|
const visuals_recorder = Recorder;
|
|
@@ -9158,7 +9460,8 @@ class Container extends util_Despot {
|
|
|
9158
9460
|
}
|
|
9159
9461
|
else runValidation = false;
|
|
9160
9462
|
if (runValidation) {
|
|
9161
|
-
const
|
|
9463
|
+
const target = event?.target;
|
|
9464
|
+
const targetName = target instanceof Element ? target.getAttribute("name") : void 0;
|
|
9162
9465
|
if (targetName) this.emit("VALIDATING", {
|
|
9163
9466
|
targetName
|
|
9164
9467
|
});
|
|
@@ -9193,27 +9496,10 @@ class Container extends util_Despot {
|
|
|
9193
9496
|
}
|
|
9194
9497
|
}
|
|
9195
9498
|
if (valid) {
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
const bccIsConfigured = "bcc" in recipients;
|
|
9200
|
-
const hasTo = recipients.to && recipients.to.length > 0;
|
|
9201
|
-
const hasCc = recipients.cc && recipients.cc.length > 0;
|
|
9202
|
-
const hasBcc = recipients.bcc && recipients.bcc.length > 0;
|
|
9203
|
-
if (toIsConfigured) {
|
|
9204
|
-
if (!hasTo) if (ccIsConfigured && bccIsConfigured) {
|
|
9205
|
-
if (!hasCc && !hasBcc) valid = false;
|
|
9206
|
-
} else if (ccIsConfigured) {
|
|
9207
|
-
if (!hasCc) valid = false;
|
|
9208
|
-
} else if (bccIsConfigured) {
|
|
9209
|
-
if (!hasBcc) valid = false;
|
|
9210
|
-
} else valid = false;
|
|
9211
|
-
} else if (ccIsConfigured) {
|
|
9212
|
-
if (!hasCc) {
|
|
9213
|
-
if (bccIsConfigured && !hasBcc) valid = false;
|
|
9214
|
-
}
|
|
9499
|
+
if (!email_isRecipientSelectionValid(this.form.getRecipients())) {
|
|
9500
|
+
valid = false;
|
|
9501
|
+
whyInvalid = "At least one recipient is required";
|
|
9215
9502
|
}
|
|
9216
|
-
if (!valid) whyInvalid = "At least one recipient is required";
|
|
9217
9503
|
}
|
|
9218
9504
|
} else {
|
|
9219
9505
|
valid = visualsValid;
|