videomail-client 16.1.1 → 16.2.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 +1 -1
- package/dist/cjs/index.cjs +1857 -1721
- package/dist/esm/index.js +1989 -1853
- package/dist/esm/types/events/params.d.ts +4 -3
- package/dist/esm/util/email/isRecipientSelectionValid.d.ts +4 -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 +1908 -1821
- package/package.json +5 -5
- /package/dist/esm/util/{trimEmail.d.ts → email/trimEmail.d.ts} +0 -0
package/dist/umd/index.js
CHANGED
|
@@ -1654,7 +1654,7 @@
|
|
|
1654
1654
|
return 0 === comparison || comparison & DOCUMENT_POSITION_CONTAINED_BY;
|
|
1655
1655
|
}
|
|
1656
1656
|
},
|
|
1657
|
-
|
|
1657
|
+
TU (__unused_rspack_module, exports1, __webpack_require__) {
|
|
1658
1658
|
function isArray(arg) {
|
|
1659
1659
|
if (Array.isArray) return Array.isArray(arg);
|
|
1660
1660
|
return '[object Array]' === objectToString(arg);
|
|
@@ -2119,7 +2119,7 @@
|
|
|
2119
2119
|
return keys;
|
|
2120
2120
|
};
|
|
2121
2121
|
module1.exports = Duplex;
|
|
2122
|
-
var util = Object.create(__webpack_require__("
|
|
2122
|
+
var util = Object.create(__webpack_require__("TU"));
|
|
2123
2123
|
util.inherits = __webpack_require__("c5");
|
|
2124
2124
|
var Readable = __webpack_require__("rp");
|
|
2125
2125
|
var Writable = __webpack_require__("pv");
|
|
@@ -2173,7 +2173,7 @@
|
|
|
2173
2173
|
"use strict";
|
|
2174
2174
|
module1.exports = PassThrough;
|
|
2175
2175
|
var Transform = __webpack_require__("It");
|
|
2176
|
-
var util = Object.create(__webpack_require__("
|
|
2176
|
+
var util = Object.create(__webpack_require__("TU"));
|
|
2177
2177
|
util.inherits = __webpack_require__("c5");
|
|
2178
2178
|
util.inherits(PassThrough, Transform);
|
|
2179
2179
|
function PassThrough(options) {
|
|
@@ -2205,7 +2205,7 @@
|
|
|
2205
2205
|
function _isUint8Array(obj) {
|
|
2206
2206
|
return Buffer.isBuffer(obj) || obj instanceof OurUint8Array;
|
|
2207
2207
|
}
|
|
2208
|
-
var util = Object.create(__webpack_require__("
|
|
2208
|
+
var util = Object.create(__webpack_require__("TU"));
|
|
2209
2209
|
util.inherits = __webpack_require__("c5");
|
|
2210
2210
|
var debugUtil = __webpack_require__("AN");
|
|
2211
2211
|
var debug = void 0;
|
|
@@ -2832,7 +2832,7 @@
|
|
|
2832
2832
|
"use strict";
|
|
2833
2833
|
module1.exports = Transform;
|
|
2834
2834
|
var Duplex = __webpack_require__("OO");
|
|
2835
|
-
var util = Object.create(__webpack_require__("
|
|
2835
|
+
var util = Object.create(__webpack_require__("TU"));
|
|
2836
2836
|
util.inherits = __webpack_require__("c5");
|
|
2837
2837
|
util.inherits(Transform, Duplex);
|
|
2838
2838
|
function afterTransform(er, data) {
|
|
@@ -2932,7 +2932,7 @@
|
|
|
2932
2932
|
].indexOf(process.version.slice(0, 5)) > -1 ? setImmediate : pna.nextTick;
|
|
2933
2933
|
var Duplex;
|
|
2934
2934
|
Writable.WritableState = WritableState;
|
|
2935
|
-
var util = Object.create(__webpack_require__("
|
|
2935
|
+
var util = Object.create(__webpack_require__("TU"));
|
|
2936
2936
|
util.inherits = __webpack_require__("c5");
|
|
2937
2937
|
var internalUtil = {
|
|
2938
2938
|
deprecate: __webpack_require__("NQ")
|
|
@@ -8254,7 +8254,7 @@
|
|
|
8254
8254
|
});
|
|
8255
8255
|
});
|
|
8256
8256
|
},
|
|
8257
|
-
|
|
8257
|
+
S (__unused_rspack_module, exports1, __webpack_require__) {
|
|
8258
8258
|
var process = __webpack_require__("zT");
|
|
8259
8259
|
var getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || function(obj) {
|
|
8260
8260
|
var keys = Object.keys(obj);
|
|
@@ -8733,7 +8733,7 @@
|
|
|
8733
8733
|
return keys;
|
|
8734
8734
|
};
|
|
8735
8735
|
module1.exports = Duplex;
|
|
8736
|
-
var util = Object.create(__webpack_require__("
|
|
8736
|
+
var util = Object.create(__webpack_require__("TU"));
|
|
8737
8737
|
util.inherits = __webpack_require__("c5");
|
|
8738
8738
|
var Readable = __webpack_require__("w");
|
|
8739
8739
|
var Writable = __webpack_require__("S5");
|
|
@@ -8787,7 +8787,7 @@
|
|
|
8787
8787
|
"use strict";
|
|
8788
8788
|
module1.exports = PassThrough;
|
|
8789
8789
|
var Transform = __webpack_require__("Ge");
|
|
8790
|
-
var util = Object.create(__webpack_require__("
|
|
8790
|
+
var util = Object.create(__webpack_require__("TU"));
|
|
8791
8791
|
util.inherits = __webpack_require__("c5");
|
|
8792
8792
|
util.inherits(PassThrough, Transform);
|
|
8793
8793
|
function PassThrough(options) {
|
|
@@ -8819,7 +8819,7 @@
|
|
|
8819
8819
|
function _isUint8Array(obj) {
|
|
8820
8820
|
return Buffer.isBuffer(obj) || obj instanceof OurUint8Array;
|
|
8821
8821
|
}
|
|
8822
|
-
var util = Object.create(__webpack_require__("
|
|
8822
|
+
var util = Object.create(__webpack_require__("TU"));
|
|
8823
8823
|
util.inherits = __webpack_require__("c5");
|
|
8824
8824
|
var debugUtil = __webpack_require__("eD");
|
|
8825
8825
|
var debug = void 0;
|
|
@@ -9446,7 +9446,7 @@
|
|
|
9446
9446
|
"use strict";
|
|
9447
9447
|
module1.exports = Transform;
|
|
9448
9448
|
var Duplex = __webpack_require__("S7");
|
|
9449
|
-
var util = Object.create(__webpack_require__("
|
|
9449
|
+
var util = Object.create(__webpack_require__("TU"));
|
|
9450
9450
|
util.inherits = __webpack_require__("c5");
|
|
9451
9451
|
util.inherits(Transform, Duplex);
|
|
9452
9452
|
function afterTransform(er, data) {
|
|
@@ -9546,7 +9546,7 @@
|
|
|
9546
9546
|
].indexOf(process.version.slice(0, 5)) > -1 ? setImmediate : pna.nextTick;
|
|
9547
9547
|
var Duplex;
|
|
9548
9548
|
Writable.WritableState = WritableState;
|
|
9549
|
-
var util = Object.create(__webpack_require__("
|
|
9549
|
+
var util = Object.create(__webpack_require__("TU"));
|
|
9550
9550
|
util.inherits = __webpack_require__("c5");
|
|
9551
9551
|
var internalUtil = {
|
|
9552
9552
|
deprecate: __webpack_require__("NQ")
|
|
@@ -11121,7 +11121,7 @@
|
|
|
11121
11121
|
var client = __webpack_require__("Q");
|
|
11122
11122
|
var client_default = /*#__PURE__*/ __webpack_require__.n(client);
|
|
11123
11123
|
var package_namespaceObject = {
|
|
11124
|
-
rE: "16.
|
|
11124
|
+
rE: "16.2.0"
|
|
11125
11125
|
};
|
|
11126
11126
|
function isAudioEnabled(options) {
|
|
11127
11127
|
return Boolean(options.audio.enabled);
|
|
@@ -11133,7 +11133,7 @@
|
|
|
11133
11133
|
function isAutoPauseEnabled(options) {
|
|
11134
11134
|
return options.enableAutoPause && options.enablePause;
|
|
11135
11135
|
}
|
|
11136
|
-
var util = __webpack_require__("
|
|
11136
|
+
var util = __webpack_require__("S");
|
|
11137
11137
|
var util_default = /*#__PURE__*/ __webpack_require__.n(util);
|
|
11138
11138
|
function getEventDetails(event) {
|
|
11139
11139
|
const eventWithErrorDetails = event;
|
|
@@ -14996,6 +14996,14 @@
|
|
|
14996
14996
|
}
|
|
14997
14997
|
}
|
|
14998
14998
|
const util_Despot = Despot;
|
|
14999
|
+
const EMAIL_SEPARATOR_REGEX = /[ ,]+/gu;
|
|
15000
|
+
function trimEmail(email) {
|
|
15001
|
+
return email.replace(EMAIL_SEPARATOR_REGEX, "");
|
|
15002
|
+
}
|
|
15003
|
+
function trimEmails(emails) {
|
|
15004
|
+
const trimmedEmails = emails.split(EMAIL_SEPARATOR_REGEX).map((item)=>item.trim()).filter(Boolean);
|
|
15005
|
+
return trimmedEmails;
|
|
15006
|
+
}
|
|
14999
15007
|
function getValidity(element) {
|
|
15000
15008
|
const validity = "validity" in element ? element.validity : void 0;
|
|
15001
15009
|
return validity;
|
|
@@ -15015,14 +15023,6 @@
|
|
|
15015
15023
|
element.style.removeProperty("display");
|
|
15016
15024
|
}
|
|
15017
15025
|
const html_showElement = showElement;
|
|
15018
|
-
const EMAIL_SEPARATOR_REGEX = /[ ,]+/gu;
|
|
15019
|
-
function trimEmail(email) {
|
|
15020
|
-
return email.replace(EMAIL_SEPARATOR_REGEX, "");
|
|
15021
|
-
}
|
|
15022
|
-
function trimEmails(emails) {
|
|
15023
|
-
const trimmedEmails = emails.split(EMAIL_SEPARATOR_REGEX).map((item)=>item.trim()).filter(Boolean);
|
|
15024
|
-
return trimmedEmails;
|
|
15025
|
-
}
|
|
15026
15026
|
const FormMethod = {
|
|
15027
15027
|
POST: "post",
|
|
15028
15028
|
PUT: "put",
|
|
@@ -16705,11 +16705,11 @@
|
|
|
16705
16705
|
const originalDocument = document1;
|
|
16706
16706
|
const currentScript = originalDocument.currentScript;
|
|
16707
16707
|
window1.DocumentFragment;
|
|
16708
|
-
const HTMLTemplateElement = window1.HTMLTemplateElement, Node = window1.Node,
|
|
16708
|
+
const HTMLTemplateElement = window1.HTMLTemplateElement, Node = window1.Node, Element1 = window1.Element, NodeFilter = window1.NodeFilter;
|
|
16709
16709
|
void 0 === window1.NamedNodeMap && (window1.NamedNodeMap || window1.MozNamedAttrMap);
|
|
16710
16710
|
window1.HTMLFormElement;
|
|
16711
16711
|
const DOMParser = window1.DOMParser, trustedTypes = window1.trustedTypes;
|
|
16712
|
-
const ElementPrototype =
|
|
16712
|
+
const ElementPrototype = Element1.prototype;
|
|
16713
16713
|
const cloneNode = lookupGetter(ElementPrototype, "cloneNode");
|
|
16714
16714
|
const remove = lookupGetter(ElementPrototype, "remove");
|
|
16715
16715
|
const removeAttributeNode = lookupGetter(ElementPrototype, "removeAttributeNode");
|
|
@@ -17792,6 +17792,19 @@
|
|
|
17792
17792
|
return DOMPurify;
|
|
17793
17793
|
}
|
|
17794
17794
|
var purify_default = createDOMPurify();
|
|
17795
|
+
function isRecipientSelectionValid(recipients) {
|
|
17796
|
+
const hasTo = Boolean(recipients.to?.length);
|
|
17797
|
+
const hasCc = Boolean(recipients.cc?.length);
|
|
17798
|
+
const hasBcc = Boolean(recipients.bcc?.length);
|
|
17799
|
+
const hasConfiguredTo = "to" in recipients;
|
|
17800
|
+
const hasConfiguredCc = "cc" in recipients;
|
|
17801
|
+
const hasConfiguredBcc = "bcc" in recipients;
|
|
17802
|
+
let isValid = true;
|
|
17803
|
+
if (hasConfiguredTo) isValid = hasTo || hasConfiguredCc && hasCc || hasConfiguredBcc && hasBcc;
|
|
17804
|
+
else if (hasConfiguredCc) isValid = hasCc || !hasConfiguredBcc || hasBcc;
|
|
17805
|
+
return isValid;
|
|
17806
|
+
}
|
|
17807
|
+
const email_isRecipientSelectionValid = isRecipientSelectionValid;
|
|
17795
17808
|
function limitHeight(height, options, calledFrom) {
|
|
17796
17809
|
const dimension = {
|
|
17797
17810
|
unit: "px"
|
|
@@ -18388,7 +18401,7 @@
|
|
|
18388
18401
|
html_showElement(this.facingModeElement);
|
|
18389
18402
|
}
|
|
18390
18403
|
}
|
|
18391
|
-
const
|
|
18404
|
+
const recorder_facingMode = FacingMode;
|
|
18392
18405
|
class PausedNote {
|
|
18393
18406
|
visuals;
|
|
18394
18407
|
options;
|
|
@@ -18609,7 +18622,7 @@
|
|
|
18609
18622
|
this.recordTimer = new recordTimer(visuals, this.recordNote, options);
|
|
18610
18623
|
const browser = util_getBrowser(options);
|
|
18611
18624
|
if (options.video.countdown) this.countdown = new countdown(visuals, options);
|
|
18612
|
-
if (options.video.facingModeButton && browser.isMobile()) this.facingMode = new
|
|
18625
|
+
if (options.video.facingModeButton && browser.isMobile()) this.facingMode = new recorder_facingMode(visuals, options);
|
|
18613
18626
|
if (options.enablePause) this.pausedNote = new pausedNote(visuals, options);
|
|
18614
18627
|
}
|
|
18615
18628
|
startRecording() {
|
|
@@ -18942,6 +18955,18 @@
|
|
|
18942
18955
|
}
|
|
18943
18956
|
}
|
|
18944
18957
|
const notifier = Notifier;
|
|
18958
|
+
function summarize(title, contents) {
|
|
18959
|
+
const lines = Object.entries(contents).sort(([keyA], [keyB])=>keyA.localeCompare(keyB)).map(([key, value])=>{
|
|
18960
|
+
const formattedValue = void 0 === value ? "undefined" : "object" == typeof value ? string_pretty(value) : value;
|
|
18961
|
+
return ` • ${key}: ${formattedValue}`;
|
|
18962
|
+
});
|
|
18963
|
+
const line = [
|
|
18964
|
+
`🔎 ${title}`,
|
|
18965
|
+
...lines
|
|
18966
|
+
].join("\n");
|
|
18967
|
+
return line;
|
|
18968
|
+
}
|
|
18969
|
+
const string_summarize = summarize;
|
|
18945
18970
|
var animitter = __webpack_require__("b6");
|
|
18946
18971
|
var animitter_default = /*#__PURE__*/ __webpack_require__.n(animitter);
|
|
18947
18972
|
var typedarray_to_buffer = __webpack_require__("E");
|
|
@@ -19005,34 +19030,6 @@
|
|
|
19005
19030
|
canvas_to_buffer_modern_r.atob = void 0;
|
|
19006
19031
|
var websocket_stream_stream = __webpack_require__("U");
|
|
19007
19032
|
var stream_default = /*#__PURE__*/ __webpack_require__.n(websocket_stream_stream);
|
|
19008
|
-
const DEFAULT_MAX_QUEUED_BYTES = 1048576;
|
|
19009
|
-
class BoundedWriteQueue {
|
|
19010
|
-
maxBytes;
|
|
19011
|
-
writes = [];
|
|
19012
|
-
queuedBytes = 0;
|
|
19013
|
-
constructor(maxBytes = DEFAULT_MAX_QUEUED_BYTES){
|
|
19014
|
-
this.maxBytes = maxBytes;
|
|
19015
|
-
}
|
|
19016
|
-
enqueue(buffer, params) {
|
|
19017
|
-
if (this.queuedBytes + buffer.length > this.maxBytes) return false;
|
|
19018
|
-
this.writes.push({
|
|
19019
|
-
buffer,
|
|
19020
|
-
params
|
|
19021
|
-
});
|
|
19022
|
-
this.queuedBytes += buffer.length;
|
|
19023
|
-
return true;
|
|
19024
|
-
}
|
|
19025
|
-
dequeue() {
|
|
19026
|
-
const write = this.writes.shift();
|
|
19027
|
-
if (write) this.queuedBytes -= write.buffer.length;
|
|
19028
|
-
return write;
|
|
19029
|
-
}
|
|
19030
|
-
clear() {
|
|
19031
|
-
this.writes.length = 0;
|
|
19032
|
-
this.queuedBytes = 0;
|
|
19033
|
-
}
|
|
19034
|
-
}
|
|
19035
|
-
const boundedWriteQueue = BoundedWriteQueue;
|
|
19036
19033
|
function getEdgeCodes(value, amount, fromEnd) {
|
|
19037
19034
|
const chars = fromEnd ? value.slice(-amount) : value.slice(0, amount);
|
|
19038
19035
|
const codes = Array.from(chars, (char)=>String(char.codePointAt(0))).join(".");
|
|
@@ -19112,1208 +19109,497 @@
|
|
|
19112
19109
|
};
|
|
19113
19110
|
}
|
|
19114
19111
|
const error_getWebSocketDiagnostic = getWebSocketDiagnostic;
|
|
19115
|
-
function figureMinHeight(height, options) {
|
|
19116
|
-
let minHeight;
|
|
19117
|
-
if (options.video.height) {
|
|
19118
|
-
minHeight = height ? Math.min(options.video.height, height) : options.video.height;
|
|
19119
|
-
if (minHeight < 1) throw error_createError({
|
|
19120
|
-
message: `Got a min height less than 1 (${minHeight})!`,
|
|
19121
|
-
options
|
|
19122
|
-
});
|
|
19123
|
-
} else minHeight = height;
|
|
19124
|
-
return minHeight;
|
|
19125
|
-
}
|
|
19126
|
-
const dimensions_figureMinHeight = figureMinHeight;
|
|
19127
|
-
function getRatio(options, videoHeight, videoWidth) {
|
|
19128
|
-
let ratio = 1;
|
|
19129
|
-
const hasVideoDimensions = videoHeight && videoWidth;
|
|
19130
|
-
const desiredHeight = options.video.height;
|
|
19131
|
-
const desiredWidth = options.video.width;
|
|
19132
|
-
const hasDesiredDimensions = desiredHeight && desiredWidth;
|
|
19133
|
-
if (hasDesiredDimensions) ratio = hasVideoDimensions ? videoHeight < desiredHeight || videoWidth < desiredWidth ? videoHeight / videoWidth : desiredHeight / desiredWidth : desiredHeight / desiredWidth;
|
|
19134
|
-
else if (hasVideoDimensions) ratio = videoHeight / videoWidth;
|
|
19135
|
-
return ratio;
|
|
19136
|
-
}
|
|
19137
|
-
const dimensions_getRatio = getRatio;
|
|
19138
|
-
function calculateHeight(responsive, videoWidth, options, ratio, element) {
|
|
19139
|
-
const dimension = {
|
|
19140
|
-
unit: "px"
|
|
19141
|
-
};
|
|
19142
|
-
let width = videoWidth;
|
|
19143
|
-
if (responsive && element) {
|
|
19144
|
-
const limitedDimension = dimensions_limitWidth(element, options, width);
|
|
19145
|
-
width = limitedDimension.value;
|
|
19146
|
-
}
|
|
19147
|
-
const chosenRatio = ratio ?? dimensions_getRatio(options, void 0, videoWidth);
|
|
19148
|
-
const height = width ? Math.round(width * chosenRatio) : void 0;
|
|
19149
|
-
if (height && Number.isInteger(height) && height < 1) throw error_createError({
|
|
19150
|
-
message: "Just calculated a height less than 1 which is wrong.",
|
|
19151
|
-
options
|
|
19152
|
-
});
|
|
19153
|
-
const minHeight = dimensions_figureMinHeight(height, options);
|
|
19154
|
-
dimension.value = minHeight;
|
|
19155
|
-
return dimension;
|
|
19156
|
-
}
|
|
19157
|
-
const dimensions_calculateHeight = calculateHeight;
|
|
19158
|
-
function calculateWidth(responsive, options, videoHeight, ratio) {
|
|
19159
|
-
const dimension = {
|
|
19160
|
-
unit: "px"
|
|
19161
|
-
};
|
|
19162
|
-
let height = dimensions_figureMinHeight(videoHeight, options);
|
|
19163
|
-
if (responsive) {
|
|
19164
|
-
const limitedDimension = dimensions_limitHeight(height, options, "calculateWidth");
|
|
19165
|
-
height = limitedDimension.value;
|
|
19166
|
-
}
|
|
19167
|
-
if (!height || height < 1) throw error_createError({
|
|
19168
|
-
message: `Height ${height} cannot be smaller than 1 when calculating width.`,
|
|
19169
|
-
options
|
|
19170
|
-
});
|
|
19171
|
-
const chosenRatio = ratio ?? dimensions_getRatio(options, videoHeight);
|
|
19172
|
-
const calculatedWidth = Math.round(height / chosenRatio);
|
|
19173
|
-
if (calculatedWidth < 1) throw error_createError({
|
|
19174
|
-
message: "Calculated width cannot be smaller than 1!",
|
|
19175
|
-
options
|
|
19176
|
-
});
|
|
19177
|
-
dimension.value = calculatedWidth;
|
|
19178
|
-
return dimension;
|
|
19179
|
-
}
|
|
19180
|
-
const dimensions_calculateWidth = calculateWidth;
|
|
19181
19112
|
function isAutomatedUserAgent(userAgent = navigator.userAgent) {
|
|
19182
19113
|
return /bot|crawler|spider|headlesschrome|phantomjs|puppeteer|playwright/iu.test(userAgent);
|
|
19183
19114
|
}
|
|
19184
19115
|
const util_isAutomatedUserAgent = isAutomatedUserAgent;
|
|
19185
|
-
|
|
19186
|
-
|
|
19187
|
-
|
|
19188
|
-
|
|
19189
|
-
|
|
19190
|
-
|
|
19191
|
-
|
|
19192
|
-
|
|
19193
|
-
|
|
19194
|
-
|
|
19195
|
-
|
|
19196
|
-
|
|
19197
|
-
|
|
19198
|
-
const videoTracks = localMediaStream.getVideoTracks();
|
|
19199
|
-
let videoTrack;
|
|
19200
|
-
if (videoTracks[0]) videoTrack = videoTracks[0];
|
|
19201
|
-
return videoTrack;
|
|
19202
|
-
}
|
|
19203
|
-
const media_getFirstVideoTrack = getFirstVideoTrack;
|
|
19204
|
-
const MEDIA_EVENTS = [
|
|
19205
|
-
"loadstart",
|
|
19206
|
-
"suspend",
|
|
19207
|
-
"progress",
|
|
19208
|
-
"abort",
|
|
19209
|
-
"emptied",
|
|
19210
|
-
"stalled",
|
|
19211
|
-
"pause",
|
|
19212
|
-
"loadeddata",
|
|
19213
|
-
"waiting",
|
|
19214
|
-
"playing",
|
|
19215
|
-
"canplay",
|
|
19216
|
-
"canplaythrough",
|
|
19217
|
-
"seeking",
|
|
19218
|
-
"seeked",
|
|
19219
|
-
"ended",
|
|
19220
|
-
"ratechange",
|
|
19221
|
-
"durationchange",
|
|
19222
|
-
"volumechange"
|
|
19223
|
-
];
|
|
19224
|
-
const mediaEvents = MEDIA_EVENTS;
|
|
19225
|
-
function _define_property(obj, key, value) {
|
|
19226
|
-
if (key in obj) Object.defineProperty(obj, key, {
|
|
19227
|
-
value: value,
|
|
19228
|
-
enumerable: true,
|
|
19229
|
-
configurable: true,
|
|
19230
|
-
writable: true
|
|
19231
|
-
});
|
|
19232
|
-
else obj[key] = value;
|
|
19233
|
-
return obj;
|
|
19234
|
-
}
|
|
19235
|
-
class AudioSample {
|
|
19236
|
-
toBuffer() {
|
|
19237
|
-
const l = this.float32Array.length;
|
|
19238
|
-
const arr = new Int16Array(l);
|
|
19239
|
-
this.float32Array.forEach((sample, i)=>{
|
|
19240
|
-
const clampedSample = Number.isFinite(sample) ? Math.max(-1, sample) : sample;
|
|
19241
|
-
arr[i] = 0x7fff * Math.min(1, clampedSample);
|
|
19116
|
+
const DEFAULT_MAX_QUEUED_BYTES = 1048576;
|
|
19117
|
+
class BoundedWriteQueue {
|
|
19118
|
+
maxBytes;
|
|
19119
|
+
writes = [];
|
|
19120
|
+
queuedBytes = 0;
|
|
19121
|
+
constructor(maxBytes = DEFAULT_MAX_QUEUED_BYTES){
|
|
19122
|
+
this.maxBytes = maxBytes;
|
|
19123
|
+
}
|
|
19124
|
+
enqueue(buffer, params) {
|
|
19125
|
+
if (this.queuedBytes + buffer.length > this.maxBytes) return false;
|
|
19126
|
+
this.writes.push({
|
|
19127
|
+
buffer,
|
|
19128
|
+
params
|
|
19242
19129
|
});
|
|
19243
|
-
|
|
19130
|
+
this.queuedBytes += buffer.length;
|
|
19131
|
+
return true;
|
|
19244
19132
|
}
|
|
19245
|
-
|
|
19246
|
-
|
|
19247
|
-
this.
|
|
19133
|
+
dequeue() {
|
|
19134
|
+
const write = this.writes.shift();
|
|
19135
|
+
if (write) this.queuedBytes -= write.buffer.length;
|
|
19136
|
+
return write;
|
|
19137
|
+
}
|
|
19138
|
+
clear() {
|
|
19139
|
+
this.writes.length = 0;
|
|
19140
|
+
this.queuedBytes = 0;
|
|
19248
19141
|
}
|
|
19249
19142
|
}
|
|
19250
|
-
const
|
|
19251
|
-
const
|
|
19252
|
-
|
|
19253
|
-
|
|
19254
|
-
|
|
19255
|
-
|
|
19256
|
-
|
|
19257
|
-
|
|
19258
|
-
|
|
19259
|
-
|
|
19260
|
-
|
|
19261
|
-
|
|
19262
|
-
|
|
19263
|
-
|
|
19264
|
-
|
|
19265
|
-
|
|
19266
|
-
|
|
19267
|
-
|
|
19143
|
+
const boundedWriteQueue = BoundedWriteQueue;
|
|
19144
|
+
const PIPE_SYMBOL = "°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸ ";
|
|
19145
|
+
class RecorderStreamWriter extends util_Despot {
|
|
19146
|
+
pendingWrites = new boundedWriteQueue();
|
|
19147
|
+
samplesCount = 0;
|
|
19148
|
+
framesCount = 0;
|
|
19149
|
+
recordingStats;
|
|
19150
|
+
confirmedFrameNumber = 0;
|
|
19151
|
+
confirmedSampleNumber = 0;
|
|
19152
|
+
backpressureTimeout;
|
|
19153
|
+
recordingDimensions;
|
|
19154
|
+
stream;
|
|
19155
|
+
streamBackpressured = false;
|
|
19156
|
+
resumeAfterDrain = false;
|
|
19157
|
+
mediaTransmissionStarted = false;
|
|
19158
|
+
recordingActive = false;
|
|
19159
|
+
recordingTransportFailed = false;
|
|
19160
|
+
connecting = false;
|
|
19161
|
+
connected = false;
|
|
19162
|
+
reconnecting = false;
|
|
19163
|
+
constructor(options){
|
|
19164
|
+
super("Recorder", options);
|
|
19268
19165
|
}
|
|
19269
|
-
|
|
19270
|
-
return
|
|
19166
|
+
hasExceededWebSocketSendBuffer(buffer, opts) {
|
|
19167
|
+
return buffer.length > 0 && !this.pendingWrites.enqueue(buffer, opts);
|
|
19271
19168
|
}
|
|
19272
|
-
|
|
19273
|
-
if (
|
|
19274
|
-
|
|
19275
|
-
|
|
19276
|
-
|
|
19277
|
-
|
|
19278
|
-
}
|
|
19279
|
-
onAudioProcess(e, cb) {
|
|
19280
|
-
if (!this.userMedia.isRecording() || this.userMedia.isPaused()) return;
|
|
19281
|
-
const float32Array = e.inputBuffer.getChannelData(0);
|
|
19282
|
-
cb(new esm(float32Array));
|
|
19283
|
-
}
|
|
19284
|
-
disconnect() {
|
|
19285
|
-
if (this.scriptProcessor) {
|
|
19286
|
-
this.scriptProcessor.onaudioprocess = null;
|
|
19287
|
-
this.scriptProcessor.disconnect();
|
|
19288
|
-
}
|
|
19289
|
-
this.audioInput?.disconnect();
|
|
19290
|
-
this.volume?.disconnect();
|
|
19291
|
-
}
|
|
19292
|
-
init(localMediaStream) {
|
|
19293
|
-
this.options.logger.debug("AudioRecorder: init()");
|
|
19294
|
-
this.disconnect();
|
|
19295
|
-
const volume = this.getAudioContext().createGain();
|
|
19296
|
-
this.volume = volume;
|
|
19297
|
-
try {
|
|
19298
|
-
this.audioInput = this.getAudioContext().createMediaStreamSource(localMediaStream);
|
|
19299
|
-
} catch (exc) {
|
|
19300
|
-
throw error_createError({
|
|
19301
|
-
message: "Webcam has no audio",
|
|
19302
|
-
exc,
|
|
19169
|
+
writeStream(buffer, opts) {
|
|
19170
|
+
if (this.stream) if (this.stream.destroyed) {
|
|
19171
|
+
this.stopPings();
|
|
19172
|
+
const err = error_createError({
|
|
19173
|
+
message: "Already disconnected",
|
|
19174
|
+
explanation: "Sorry, connection to the server has been destroyed. Please reload.",
|
|
19303
19175
|
options: this.options
|
|
19304
19176
|
});
|
|
19305
|
-
|
|
19306
|
-
|
|
19307
|
-
|
|
19308
|
-
|
|
19309
|
-
|
|
19310
|
-
options: this.options
|
|
19311
|
-
});
|
|
19312
|
-
if (!Number.isFinite(this.options.audio.volume) || this.options.audio.volume < 0 || this.options.audio.volume > 1) throw error_createError({
|
|
19313
|
-
message: `Audio volume must be between zero and one. The current volume is ${this.options.audio.volume}.`,
|
|
19314
|
-
options: this.options
|
|
19315
|
-
});
|
|
19316
|
-
volume.gain.value = this.options.audio.volume;
|
|
19317
|
-
this.scriptProcessor = this.getAudioContext().createScriptProcessor(bufferSize, CHANNELS, CHANNELS);
|
|
19318
|
-
this.audioInput.connect(volume);
|
|
19319
|
-
volume.connect(this.scriptProcessor);
|
|
19320
|
-
this.scriptProcessor.connect(this.getAudioContext().destination);
|
|
19321
|
-
}
|
|
19322
|
-
record(cb) {
|
|
19323
|
-
this.options.logger.debug("AudioRecorder: record()");
|
|
19324
|
-
if (this.scriptProcessor) this.scriptProcessor.onaudioprocess = (e)=>{
|
|
19325
|
-
this.onAudioProcess(e, cb);
|
|
19326
|
-
};
|
|
19327
|
-
}
|
|
19328
|
-
async resume() {
|
|
19329
|
-
const context = this.getAudioContext();
|
|
19330
|
-
if ("running" !== context.state) await context.resume();
|
|
19331
|
-
}
|
|
19332
|
-
stop() {
|
|
19333
|
-
this.options.logger.debug("AudioRecorder: stop()");
|
|
19334
|
-
this.disconnect();
|
|
19335
|
-
if (this.hasAudioContext()) this.getAudioContext().close().then(()=>{
|
|
19336
|
-
this.options.logger.debug("AudioRecorder: audio context is closed");
|
|
19337
|
-
this.vcAudioContext = void 0;
|
|
19338
|
-
}).catch((err)=>{
|
|
19339
|
-
if (err instanceof Error) return void this.options.logger.error(error_createError({
|
|
19340
|
-
err,
|
|
19177
|
+
this.handleWriteFailure(err);
|
|
19178
|
+
} else if (this.streamBackpressured) {
|
|
19179
|
+
if (this.hasExceededWebSocketSendBuffer(buffer, opts)) this.failMediaTransmission(error_createError({
|
|
19180
|
+
message: "WebSocket send buffer limit exceeded",
|
|
19181
|
+
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.",
|
|
19341
19182
|
options: this.options
|
|
19342
19183
|
}));
|
|
19343
|
-
|
|
19344
|
-
});
|
|
19345
|
-
}
|
|
19346
|
-
getSampleRate() {
|
|
19347
|
-
if (this.hasAudioContext()) return this.getAudioContext().sampleRate;
|
|
19348
|
-
return -1;
|
|
19349
|
-
}
|
|
19350
|
-
}
|
|
19351
|
-
const media_AudioRecorder = AudioRecorder;
|
|
19352
|
-
const VIRTUAL_KEYWORDS = [
|
|
19353
|
-
"obs",
|
|
19354
|
-
"virtual",
|
|
19355
|
-
"manycam",
|
|
19356
|
-
"vcam",
|
|
19357
|
-
"splitcam",
|
|
19358
|
-
"droidcam"
|
|
19359
|
-
];
|
|
19360
|
-
function isVirtualCamera(videoTrack) {
|
|
19361
|
-
const capabilities = videoTrack.getCapabilities();
|
|
19362
|
-
if (!capabilities.frameRate) return true;
|
|
19363
|
-
const isVirtual = VIRTUAL_KEYWORDS.some((keyword)=>videoTrack.label.toLowerCase().includes(keyword));
|
|
19364
|
-
return isVirtual;
|
|
19365
|
-
}
|
|
19366
|
-
const util_isVirtualCamera = isVirtualCamera;
|
|
19367
|
-
const EVENT_ASCII = "|—O—|";
|
|
19368
|
-
class UserMedia extends util_Despot {
|
|
19369
|
-
recorder;
|
|
19370
|
-
rawVisualUserMedia;
|
|
19371
|
-
paused = false;
|
|
19372
|
-
recording = false;
|
|
19373
|
-
audioRecorder;
|
|
19374
|
-
currentVisualStream;
|
|
19375
|
-
onPlayReached = false;
|
|
19376
|
-
onLoadedMetaDataReached = false;
|
|
19377
|
-
playingPromiseReached = false;
|
|
19378
|
-
videoTrackLabel;
|
|
19379
|
-
clearInitializationListeners;
|
|
19380
|
-
initializationId = 0;
|
|
19381
|
-
outputEvent = (e)=>{
|
|
19382
|
-
this.logEvent(e.type, {
|
|
19383
|
-
readyState: this.rawVisualUserMedia?.readyState
|
|
19384
|
-
});
|
|
19385
|
-
this.rawVisualUserMedia?.removeEventListener(e.type, this.outputEvent);
|
|
19386
|
-
};
|
|
19387
|
-
handleMediaError = (event)=>{
|
|
19388
|
-
this.options.logger.warn(`Caught video element error event: ${string_pretty(event)}`);
|
|
19389
|
-
};
|
|
19390
|
-
constructor(recorder, options){
|
|
19391
|
-
super("UserMedia", options);
|
|
19392
|
-
this.recorder = recorder;
|
|
19393
|
-
this.rawVisualUserMedia = recorder.getRawVisualUserMedia();
|
|
19394
|
-
mediaEvents.forEach((eventName)=>{
|
|
19395
|
-
this.rawVisualUserMedia?.addEventListener(eventName, this.outputEvent, false);
|
|
19396
|
-
});
|
|
19184
|
+
} else this.writeToStream(this.stream, buffer, opts);
|
|
19397
19185
|
}
|
|
19398
|
-
|
|
19399
|
-
|
|
19400
|
-
|
|
19401
|
-
const
|
|
19402
|
-
|
|
19403
|
-
|
|
19404
|
-
|
|
19186
|
+
writeToStream(stream, buffer, opts) {
|
|
19187
|
+
const onFlushedCallback = opts?.onFlushedCallback;
|
|
19188
|
+
try {
|
|
19189
|
+
const canContinue = stream.write(buffer, ()=>{
|
|
19190
|
+
if (!onFlushedCallback) return;
|
|
19191
|
+
try {
|
|
19192
|
+
onFlushedCallback(opts);
|
|
19193
|
+
} catch (exc) {
|
|
19194
|
+
const err = error_createError({
|
|
19195
|
+
message: "Failed to write stream buffer",
|
|
19196
|
+
explanation: `stream.write() failed because of ${string_pretty(exc)}`,
|
|
19197
|
+
options: this.options,
|
|
19198
|
+
exc
|
|
19199
|
+
});
|
|
19200
|
+
this.emit("ERROR", {
|
|
19201
|
+
err
|
|
19202
|
+
});
|
|
19203
|
+
}
|
|
19405
19204
|
});
|
|
19406
|
-
|
|
19407
|
-
|
|
19408
|
-
|
|
19409
|
-
|
|
19410
|
-
|
|
19411
|
-
}
|
|
19412
|
-
setVisualStream(localMediaStream) {
|
|
19413
|
-
if (localMediaStream) this.attachMediaStream(localMediaStream);
|
|
19414
|
-
else {
|
|
19415
|
-
if (this.rawVisualUserMedia) {
|
|
19416
|
-
this.rawVisualUserMedia.srcObject = null;
|
|
19417
|
-
this.rawVisualUserMedia.removeAttribute("src");
|
|
19205
|
+
if (!canContinue) {
|
|
19206
|
+
this.streamBackpressured = true;
|
|
19207
|
+
this.startBackpressureTimeout();
|
|
19208
|
+
this.pauseForBackpressure();
|
|
19209
|
+
return false;
|
|
19418
19210
|
}
|
|
19419
|
-
|
|
19211
|
+
return true;
|
|
19212
|
+
} catch (exc) {
|
|
19213
|
+
const err = error_createError({
|
|
19214
|
+
message: "Failed writing to server",
|
|
19215
|
+
explanation: `stream.write() failed because of ${string_pretty(exc)}`,
|
|
19216
|
+
options: this.options,
|
|
19217
|
+
exc
|
|
19218
|
+
});
|
|
19219
|
+
this.handleWriteFailure(err);
|
|
19220
|
+
return false;
|
|
19420
19221
|
}
|
|
19421
19222
|
}
|
|
19422
|
-
|
|
19423
|
-
if (
|
|
19424
|
-
|
|
19425
|
-
|
|
19223
|
+
flushPendingWrites(stream) {
|
|
19224
|
+
if (this.isStaleStream(stream)) return;
|
|
19225
|
+
this.streamBackpressured = false;
|
|
19226
|
+
this.clearBackpressureTimeout();
|
|
19227
|
+
let pendingWrite = this.pendingWrites.dequeue();
|
|
19228
|
+
while(pendingWrite){
|
|
19229
|
+
if (!this.writeToStream(stream, pendingWrite.buffer, pendingWrite.params)) return;
|
|
19230
|
+
pendingWrite = this.pendingWrites.dequeue();
|
|
19231
|
+
}
|
|
19232
|
+
this.resumeRecordingAfterDrain();
|
|
19426
19233
|
}
|
|
19427
|
-
|
|
19428
|
-
|
|
19429
|
-
const audioReady = !isAudioEnabled(this.options) || this.currentVisualStream?.getAudioTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
|
|
19430
|
-
return Boolean(this.currentVisualStream?.active && videoReady && audioReady);
|
|
19234
|
+
isStaleStream(stream) {
|
|
19235
|
+
return this.stream !== stream || stream.destroyed;
|
|
19431
19236
|
}
|
|
19432
|
-
|
|
19433
|
-
|
|
19434
|
-
return !this.currentVisualStream?.active;
|
|
19237
|
+
canPauseForBackpressure() {
|
|
19238
|
+
return this.recordingActive && !this.isUserMediaPaused() && this.isCaptureLoopRunning();
|
|
19435
19239
|
}
|
|
19436
|
-
|
|
19437
|
-
if (!this.
|
|
19438
|
-
|
|
19439
|
-
|
|
19240
|
+
pauseForBackpressure() {
|
|
19241
|
+
if (!this.canPauseForBackpressure()) return;
|
|
19242
|
+
this.resumeAfterDrain = true;
|
|
19243
|
+
this.stopCaptureLoop();
|
|
19440
19244
|
}
|
|
19441
|
-
|
|
19442
|
-
this.
|
|
19245
|
+
resumeRecordingAfterDrain() {
|
|
19246
|
+
if (!this.resumeAfterDrain) return;
|
|
19247
|
+
this.resumeAfterDrain = false;
|
|
19248
|
+
if (this.canResumeAfterDrain()) this.startCaptureLoop();
|
|
19443
19249
|
}
|
|
19444
|
-
|
|
19445
|
-
this.
|
|
19446
|
-
|
|
19447
|
-
|
|
19448
|
-
|
|
19250
|
+
canResumeAfterDrain() {
|
|
19251
|
+
return this.recordingActive && !this.isUserMediaPaused();
|
|
19252
|
+
}
|
|
19253
|
+
handleWriteFailure(err) {
|
|
19254
|
+
if (this.mediaTransmissionStarted) return void this.failMediaTransmission(err);
|
|
19255
|
+
this.emit("ERROR", {
|
|
19256
|
+
err
|
|
19449
19257
|
});
|
|
19450
|
-
this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
|
|
19451
19258
|
}
|
|
19452
|
-
|
|
19453
|
-
this.
|
|
19454
|
-
this.
|
|
19259
|
+
failMediaTransmission(err) {
|
|
19260
|
+
if (this.recordingTransportFailed) return;
|
|
19261
|
+
this.recordingTransportFailed = true;
|
|
19262
|
+
this.recordingActive = false;
|
|
19263
|
+
this.mediaTransmissionStarted = false;
|
|
19264
|
+
this.streamBackpressured = false;
|
|
19265
|
+
this.resumeAfterDrain = false;
|
|
19266
|
+
this.pendingWrites.clear();
|
|
19267
|
+
this.connected = false;
|
|
19268
|
+
this.connecting = false;
|
|
19269
|
+
this.reconnecting = false;
|
|
19270
|
+
this.clearReconnectTimeout();
|
|
19271
|
+
this.clearBackpressureTimeout();
|
|
19272
|
+
this.discardSocket();
|
|
19273
|
+
this.reset();
|
|
19274
|
+
this.framesCount = this.samplesCount = 0;
|
|
19275
|
+
this.confirmedFrameNumber = this.confirmedSampleNumber = 0;
|
|
19276
|
+
delete this.recordingStats;
|
|
19277
|
+
this.recordingDimensions = void 0;
|
|
19278
|
+
this.emit("ERROR", {
|
|
19279
|
+
err
|
|
19280
|
+
});
|
|
19455
19281
|
}
|
|
19456
|
-
|
|
19457
|
-
this.
|
|
19458
|
-
this.
|
|
19459
|
-
|
|
19460
|
-
|
|
19461
|
-
|
|
19462
|
-
|
|
19463
|
-
|
|
19464
|
-
this.onLoadedMetaDataReached = false;
|
|
19465
|
-
this.playingPromiseReached = false;
|
|
19466
|
-
this.videoTrackLabel = void 0;
|
|
19467
|
-
if (isAudioEnabled(this.options)) this.audioRecorder ??= new media_AudioRecorder(this, this.options);
|
|
19468
|
-
const unloadAllEventListeners = ()=>{
|
|
19469
|
-
this.options.logger.debug("UserMedia: unloadAllEventListeners()");
|
|
19470
|
-
this.unloadRemainingEventListeners();
|
|
19471
|
-
this.removeListener("SENDING_FIRST_FRAME");
|
|
19472
|
-
this.rawVisualUserMedia?.removeEventListener("play", onPlay);
|
|
19473
|
-
this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
|
|
19474
|
-
};
|
|
19475
|
-
this.clearInitializationListeners = ()=>{
|
|
19476
|
-
this.rawVisualUserMedia?.removeEventListener("play", onPlay);
|
|
19477
|
-
this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
|
|
19478
|
-
this.clearInitializationListeners = void 0;
|
|
19479
|
-
};
|
|
19480
|
-
const play = ()=>{
|
|
19481
|
-
try {
|
|
19482
|
-
this.rawVisualUserMedia?.load();
|
|
19483
|
-
if (this.rawVisualUserMedia?.paused) {
|
|
19484
|
-
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)}`);
|
|
19485
|
-
this.rawVisualUserMedia.play().then(()=>{
|
|
19486
|
-
if (initializationId !== this.initializationId) return;
|
|
19487
|
-
if (!this.playingPromiseReached) {
|
|
19488
|
-
this.options.logger.debug("UserMedia: play promise successful. Playing now.");
|
|
19489
|
-
this.playingPromiseReached = true;
|
|
19490
|
-
}
|
|
19491
|
-
}).catch((exc)=>{
|
|
19492
|
-
if (initializationId !== this.initializationId) return;
|
|
19493
|
-
if (exc instanceof Error && !params?.recovering) this.options.logger.warn(`Caught pending user media promise exception: ${exc.toString()}`);
|
|
19494
|
-
else {
|
|
19495
|
-
unloadAllEventListeners();
|
|
19496
|
-
endedEarlyCallback(error_createError({
|
|
19497
|
-
message: "Failed to play user media upon play event.",
|
|
19498
|
-
exc,
|
|
19499
|
-
options: this.options
|
|
19500
|
-
}));
|
|
19501
|
-
}
|
|
19502
|
-
});
|
|
19503
|
-
}
|
|
19504
|
-
} catch (exc) {
|
|
19505
|
-
unloadAllEventListeners();
|
|
19506
|
-
endedEarlyCallback(exc);
|
|
19507
|
-
}
|
|
19508
|
-
};
|
|
19509
|
-
const fireCallbacks = ()=>{
|
|
19510
|
-
const readyState = this.rawVisualUserMedia?.readyState;
|
|
19511
|
-
this.options.logger.debug(`UserMedia: fireCallbacks(readyState=${readyState}, onPlayReached=${this.onPlayReached}, onLoadedMetaDataReached=${this.onLoadedMetaDataReached})`);
|
|
19512
|
-
if (this.onPlayReached && this.onLoadedMetaDataReached) if (this.audioRecorder) try {
|
|
19513
|
-
this.audioRecorder.init(localMediaStream);
|
|
19514
|
-
const onAudioReady = ()=>{
|
|
19515
|
-
if (initializationId !== this.initializationId || this.currentVisualStream !== localMediaStream) return;
|
|
19516
|
-
if (this.recording) this.audioRecord(audioCallback);
|
|
19517
|
-
else this.on("SENDING_FIRST_FRAME", ()=>{
|
|
19518
|
-
this.audioRecord(audioCallback);
|
|
19519
|
-
});
|
|
19520
|
-
this.clearInitializationListeners?.();
|
|
19521
|
-
videoCallback();
|
|
19522
|
-
};
|
|
19523
|
-
if (params?.recovering) this.audioRecorder.resume().then(onAudioReady).catch(endedEarlyCallback);
|
|
19524
|
-
else onAudioReady();
|
|
19525
|
-
} catch (exc) {
|
|
19526
|
-
unloadAllEventListeners();
|
|
19527
|
-
this.stop(void 0, {
|
|
19528
|
-
preserveRecordingState: Boolean(params?.recovering)
|
|
19529
|
-
});
|
|
19530
|
-
endedEarlyCallback(exc);
|
|
19531
|
-
}
|
|
19532
|
-
else {
|
|
19533
|
-
this.clearInitializationListeners?.();
|
|
19534
|
-
videoCallback();
|
|
19535
|
-
}
|
|
19536
|
-
};
|
|
19537
|
-
const onPlay = ()=>{
|
|
19538
|
-
try {
|
|
19539
|
-
this.logEvent("play", {
|
|
19540
|
-
readyState: this.rawVisualUserMedia?.readyState,
|
|
19541
|
-
audio: isAudioEnabled(this.options),
|
|
19542
|
-
width: this.rawVisualUserMedia?.width,
|
|
19543
|
-
height: this.rawVisualUserMedia?.height,
|
|
19544
|
-
videoWidth: this.rawVisualUserMedia?.videoWidth,
|
|
19545
|
-
videoHeight: this.rawVisualUserMedia?.videoHeight
|
|
19546
|
-
});
|
|
19547
|
-
this.rawVisualUserMedia?.removeEventListener("play", onPlay);
|
|
19548
|
-
if (this.hasEnded() || this.hasInvalidDimensions()) endedEarlyCallback(error_createError({
|
|
19549
|
-
message: "Already busy",
|
|
19550
|
-
explanation: "Probably another browser window is using your webcam?",
|
|
19551
|
-
options: this.options
|
|
19552
|
-
}));
|
|
19553
|
-
else {
|
|
19554
|
-
this.onPlayReached = true;
|
|
19555
|
-
fireCallbacks();
|
|
19556
|
-
}
|
|
19557
|
-
} catch (exc) {
|
|
19558
|
-
unloadAllEventListeners();
|
|
19559
|
-
endedEarlyCallback(exc);
|
|
19560
|
-
}
|
|
19561
|
-
};
|
|
19562
|
-
const onLoadedMetaData = ()=>{
|
|
19563
|
-
this.logEvent("loadedmetadata", {
|
|
19564
|
-
readyState: this.rawVisualUserMedia?.readyState,
|
|
19565
|
-
paused: this.rawVisualUserMedia?.paused,
|
|
19566
|
-
width: this.rawVisualUserMedia?.width,
|
|
19567
|
-
height: this.rawVisualUserMedia?.height,
|
|
19568
|
-
videoWidth: this.rawVisualUserMedia?.videoWidth,
|
|
19569
|
-
videoHeight: this.rawVisualUserMedia?.videoHeight
|
|
19570
|
-
});
|
|
19571
|
-
this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
|
|
19572
|
-
if (!this.hasEnded() && !this.hasInvalidDimensions()) {
|
|
19573
|
-
this.emit("LOADED_META_DATA");
|
|
19574
|
-
this.onLoadedMetaDataReached = true;
|
|
19575
|
-
fireCallbacks();
|
|
19576
|
-
}
|
|
19577
|
-
};
|
|
19578
|
-
try {
|
|
19579
|
-
const videoTrack = media_getFirstVideoTrack(localMediaStream);
|
|
19580
|
-
this.videoTrackLabel = videoTrack?.label;
|
|
19581
|
-
if (videoTrack) if (videoTrack.enabled) if (util_isVirtualCamera(videoTrack)) throw error_createError({
|
|
19582
|
-
message: "Virtual cameras are not allowed.",
|
|
19583
|
-
explanation: `Please use a real camera, not "${videoTrack.label}".`,
|
|
19584
|
-
options: this.options
|
|
19585
|
-
});
|
|
19586
|
-
else {
|
|
19587
|
-
let description = "";
|
|
19588
|
-
if (videoTrack.label.length > 0) description = description.concat(videoTrack.label);
|
|
19589
|
-
description = description.concat(` with enabled=${videoTrack.enabled}, muted=${videoTrack.muted}, readyState=${videoTrack.readyState}`);
|
|
19590
|
-
this.options.logger.debug(`UserMedia: ${videoTrack.kind} detected. ${description}`);
|
|
19591
|
-
}
|
|
19592
|
-
else throw error_createError({
|
|
19593
|
-
message: "Webcam is disabled",
|
|
19594
|
-
explanation: "The video track seems to be disabled. Enable it in your system.",
|
|
19282
|
+
startBackpressureTimeout() {
|
|
19283
|
+
if (!this.shouldStartBackpressureTimeout()) return;
|
|
19284
|
+
this.backpressureTimeout = window.setTimeout(()=>{
|
|
19285
|
+
this.backpressureTimeout = void 0;
|
|
19286
|
+
if (this.shouldIgnoreBackpressureTimeout()) return;
|
|
19287
|
+
const err = error_createError({
|
|
19288
|
+
message: "WebSocket write timed out",
|
|
19289
|
+
explanation: "The server stopped accepting recording data. The recording has been stopped; please check your connection and start a new one.",
|
|
19595
19290
|
options: this.options
|
|
19596
19291
|
});
|
|
19597
|
-
|
|
19598
|
-
|
|
19599
|
-
this.rawVisualUserMedia?.addEventListener("play", onPlay);
|
|
19600
|
-
this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
|
|
19601
|
-
this.rawVisualUserMedia?.addEventListener("error", this.handleMediaError);
|
|
19602
|
-
this.setVisualStream(localMediaStream);
|
|
19603
|
-
play();
|
|
19604
|
-
} catch (exc) {
|
|
19605
|
-
unloadAllEventListeners();
|
|
19606
|
-
endedEarlyCallback(exc);
|
|
19607
|
-
}
|
|
19292
|
+
this.failMediaTransmission(err);
|
|
19293
|
+
}, this.options.timeouts.connection);
|
|
19608
19294
|
}
|
|
19609
|
-
|
|
19610
|
-
return this.
|
|
19295
|
+
shouldStartBackpressureTimeout() {
|
|
19296
|
+
return void 0 === this.backpressureTimeout && this.mediaTransmissionStarted;
|
|
19611
19297
|
}
|
|
19612
|
-
|
|
19613
|
-
return
|
|
19298
|
+
shouldIgnoreBackpressureTimeout() {
|
|
19299
|
+
return !this.streamBackpressured || this.recordingTransportFailed;
|
|
19614
19300
|
}
|
|
19615
|
-
|
|
19616
|
-
|
|
19617
|
-
|
|
19618
|
-
|
|
19619
|
-
|
|
19620
|
-
|
|
19621
|
-
|
|
19622
|
-
|
|
19623
|
-
|
|
19624
|
-
|
|
19625
|
-
});
|
|
19626
|
-
this.setVisualStream();
|
|
19627
|
-
this.audioRecorder?.stop();
|
|
19628
|
-
this.audioRecorder = void 0;
|
|
19301
|
+
failInterruptedRecording() {
|
|
19302
|
+
const unconfirmedFrames = Math.max(0, this.framesCount - this.confirmedFrameNumber);
|
|
19303
|
+
const unconfirmedSamples = Math.max(0, this.samplesCount - this.confirmedSampleNumber);
|
|
19304
|
+
const err = error_createError({
|
|
19305
|
+
message: "Recording interrupted",
|
|
19306
|
+
explanation: "The server connection closed before this recording completed. It cannot be safely resumed, so it has been stopped. Please start a new recording.",
|
|
19307
|
+
options: this.options,
|
|
19308
|
+
cause: {
|
|
19309
|
+
unconfirmedFrames,
|
|
19310
|
+
unconfirmedSamples
|
|
19629
19311
|
}
|
|
19630
|
-
|
|
19631
|
-
|
|
19632
|
-
this.emit("ERROR", {
|
|
19633
|
-
exc
|
|
19634
|
-
});
|
|
19635
|
-
}
|
|
19312
|
+
});
|
|
19313
|
+
this.failMediaTransmission(err);
|
|
19636
19314
|
}
|
|
19637
|
-
|
|
19638
|
-
|
|
19639
|
-
|
|
19640
|
-
if (rawWidthDimension.value) canvas.width = rawWidthDimension.value;
|
|
19641
|
-
const rawHeight = this.getRawHeight(true);
|
|
19642
|
-
if (rawHeight) canvas.height = rawHeight;
|
|
19643
|
-
return canvas;
|
|
19315
|
+
handleStreamDrain(stream) {
|
|
19316
|
+
this.options.logger.debug(`${PIPE_SYMBOL}Stream *drain* event emitted`);
|
|
19317
|
+
this.flushPendingWrites(stream);
|
|
19644
19318
|
}
|
|
19645
|
-
|
|
19646
|
-
if (
|
|
19647
|
-
|
|
19319
|
+
clearBackpressureTimeout() {
|
|
19320
|
+
if (void 0 === this.backpressureTimeout) return;
|
|
19321
|
+
window.clearTimeout(this.backpressureTimeout);
|
|
19322
|
+
this.backpressureTimeout = void 0;
|
|
19648
19323
|
}
|
|
19649
|
-
|
|
19650
|
-
|
|
19651
|
-
|
|
19324
|
+
discardSocket() {
|
|
19325
|
+
this.options.logger.debug("Recorder: discarding socket stream ...");
|
|
19326
|
+
this.clearBackpressureTimeout();
|
|
19327
|
+
this.pendingWrites.clear();
|
|
19328
|
+
this.streamBackpressured = false;
|
|
19329
|
+
this.resumeAfterDrain = false;
|
|
19330
|
+
const stream = this.stream;
|
|
19331
|
+
this.stream = void 0;
|
|
19332
|
+
if (stream && !stream.destroyed) stream.destroy();
|
|
19652
19333
|
}
|
|
19653
|
-
|
|
19654
|
-
|
|
19655
|
-
|
|
19334
|
+
}
|
|
19335
|
+
const streamWriter = RecorderStreamWriter;
|
|
19336
|
+
var Buffer = __webpack_require__("dY").Buffer;
|
|
19337
|
+
class RecorderConnection extends streamWriter {
|
|
19338
|
+
reconnectTimeout;
|
|
19339
|
+
connectionTimeout;
|
|
19340
|
+
userMediaLoaded;
|
|
19341
|
+
userMediaLoading = false;
|
|
19342
|
+
unloaded;
|
|
19343
|
+
connectionFailed = false;
|
|
19344
|
+
reconnectAttempts = 0;
|
|
19345
|
+
blocking = false;
|
|
19346
|
+
connectingStartedAt;
|
|
19347
|
+
lastCloseEvent;
|
|
19348
|
+
lastSocketError;
|
|
19349
|
+
pingInterval;
|
|
19350
|
+
constructor(options){
|
|
19351
|
+
super(options);
|
|
19656
19352
|
}
|
|
19657
|
-
|
|
19658
|
-
|
|
19659
|
-
const widthDimension = {
|
|
19660
|
-
unit: "px"
|
|
19661
|
-
};
|
|
19662
|
-
rawWidth = responsive ? this.getVideoWidth() : this.options.video.width ? this.options.video.width : this.getVideoWidth();
|
|
19663
|
-
if (responsive) {
|
|
19664
|
-
const widthDimension = this.recorder.limitWidth(rawWidth);
|
|
19665
|
-
rawWidth = widthDimension?.value;
|
|
19666
|
-
}
|
|
19667
|
-
widthDimension.value = rawWidth;
|
|
19668
|
-
return widthDimension;
|
|
19353
|
+
canReconnectAfterStreamClose() {
|
|
19354
|
+
return Boolean(this.connected && this.userMediaLoaded);
|
|
19669
19355
|
}
|
|
19670
|
-
|
|
19671
|
-
|
|
19672
|
-
if (responsive) {
|
|
19673
|
-
rawHeight = this.options.video.height || this.getVideoHeight();
|
|
19674
|
-
const ratio = this.recorder.getRatio();
|
|
19675
|
-
const rawWidth = this.getRawWidth(responsive);
|
|
19676
|
-
if (void 0 !== ratio && rawWidth.value) rawHeight = rawWidth.value * ratio;
|
|
19677
|
-
} else rawHeight = this.options.video.height ? this.options.video.height : this.getVideoHeight();
|
|
19678
|
-
if (responsive) {
|
|
19679
|
-
const heightDimension = this.recorder.limitHeight(rawHeight);
|
|
19680
|
-
rawHeight = heightDimension.value;
|
|
19681
|
-
}
|
|
19682
|
-
return rawHeight;
|
|
19356
|
+
shouldReportStreamClose() {
|
|
19357
|
+
return !this.connecting && !this.blocking;
|
|
19683
19358
|
}
|
|
19684
|
-
|
|
19685
|
-
|
|
19359
|
+
handleStreamClose(url2Connect) {
|
|
19360
|
+
const debugLine = string_summarize(`${PIPE_SYMBOL}Stream has closed:`, {
|
|
19361
|
+
connecting: this.connecting,
|
|
19362
|
+
connected: this.connected,
|
|
19363
|
+
userMediaLoaded: this.userMediaLoaded,
|
|
19364
|
+
blocking: this.blocking
|
|
19365
|
+
});
|
|
19366
|
+
this.options.logger.debug(debugLine);
|
|
19367
|
+
const canReconnect = this.canReconnectAfterStreamClose();
|
|
19368
|
+
this.connected = false;
|
|
19369
|
+
this.stopPings();
|
|
19370
|
+
if (this.shouldIgnoreStreamClose()) return;
|
|
19371
|
+
if (this.mediaTransmissionStarted) this.failInterruptedRecording();
|
|
19372
|
+
else if (canReconnect) {
|
|
19373
|
+
this.pendingWrites.clear();
|
|
19374
|
+
this.streamBackpressured = false;
|
|
19375
|
+
this.resumeAfterDrain = false;
|
|
19376
|
+
this.reconnecting = true;
|
|
19377
|
+
this.initSocket();
|
|
19378
|
+
} else if (this.shouldReportStreamClose()) window.setTimeout(()=>{
|
|
19379
|
+
this.handleConnectionFailure({
|
|
19380
|
+
url2Connect,
|
|
19381
|
+
cause: "closed"
|
|
19382
|
+
});
|
|
19383
|
+
}, 0);
|
|
19686
19384
|
}
|
|
19687
|
-
|
|
19688
|
-
this.
|
|
19385
|
+
shouldIgnoreStreamClose() {
|
|
19386
|
+
return this.recordingTransportFailed || Boolean(this.unloaded) || this.validate();
|
|
19689
19387
|
}
|
|
19690
|
-
|
|
19691
|
-
|
|
19388
|
+
handleStreamError(url2Connect, err) {
|
|
19389
|
+
if ("u" > typeof Event && err instanceof Event) this.lastSocketError = error_getEventDetails(err);
|
|
19390
|
+
else this.lastSocketError = serializeError(err);
|
|
19391
|
+
const debugLine = string_summarize(`${PIPE_SYMBOL}Stream *error* event emitted:`, {
|
|
19392
|
+
error: serializeError(err)
|
|
19393
|
+
});
|
|
19394
|
+
this.options.logger.debug(debugLine);
|
|
19395
|
+
if (this.validate()) {
|
|
19396
|
+
this.connected = false;
|
|
19397
|
+
this.stopPings();
|
|
19398
|
+
this.discardSocket();
|
|
19399
|
+
return;
|
|
19400
|
+
}
|
|
19401
|
+
if (!this.connected) return void this.handleConnectionFailure({
|
|
19402
|
+
url2Connect,
|
|
19403
|
+
cause: "error"
|
|
19404
|
+
});
|
|
19405
|
+
const streamError = err instanceof Error ? err : new Error("WebSocket stream emitted an error event", {
|
|
19406
|
+
cause: this.lastSocketError ?? err
|
|
19407
|
+
});
|
|
19408
|
+
if (this.mediaTransmissionStarted) return void this.failInterruptedRecording();
|
|
19409
|
+
this.emit("ERROR", {
|
|
19410
|
+
err: error_createError({
|
|
19411
|
+
message: "WebSocket stream error",
|
|
19412
|
+
explanation: `The WebSocket stream emitted an error event. Details: ${string_pretty(err)}`,
|
|
19413
|
+
options: this.options,
|
|
19414
|
+
exc: streamError
|
|
19415
|
+
})
|
|
19416
|
+
});
|
|
19692
19417
|
}
|
|
19693
|
-
|
|
19694
|
-
this.
|
|
19418
|
+
sendPings() {
|
|
19419
|
+
this.stopPings();
|
|
19420
|
+
this.pingInterval = window.setInterval(()=>{
|
|
19421
|
+
this.options.logger.debug("Recorder: pinging...");
|
|
19422
|
+
this.writeStream(Buffer.from(""));
|
|
19423
|
+
}, this.options.timeouts.pingInterval);
|
|
19695
19424
|
}
|
|
19696
|
-
|
|
19697
|
-
this.
|
|
19425
|
+
stopPings() {
|
|
19426
|
+
if (void 0 === this.pingInterval) return;
|
|
19427
|
+
this.options.logger.debug("Recorder: stopping pings...");
|
|
19428
|
+
window.clearInterval(this.pingInterval);
|
|
19429
|
+
this.pingInterval = void 0;
|
|
19698
19430
|
}
|
|
19699
|
-
|
|
19700
|
-
|
|
19431
|
+
clearConnectionTimeout() {
|
|
19432
|
+
if (void 0 === this.connectionTimeout) return;
|
|
19433
|
+
this.options.logger.debug("Recorder: clearConnectionTimeout()");
|
|
19434
|
+
window.clearTimeout(this.connectionTimeout);
|
|
19435
|
+
this.connectionTimeout = void 0;
|
|
19701
19436
|
}
|
|
19702
|
-
|
|
19703
|
-
if (this.
|
|
19704
|
-
|
|
19437
|
+
clearReconnectTimeout() {
|
|
19438
|
+
if (void 0 === this.reconnectTimeout) return;
|
|
19439
|
+
this.options.logger.debug("Recorder: clearReconnectTimeout()");
|
|
19440
|
+
window.clearTimeout(this.reconnectTimeout);
|
|
19441
|
+
this.reconnectTimeout = void 0;
|
|
19705
19442
|
}
|
|
19706
|
-
|
|
19707
|
-
return
|
|
19708
|
-
|
|
19709
|
-
muted: this.rawVisualUserMedia?.muted,
|
|
19710
|
-
width: this.rawVisualUserMedia?.width,
|
|
19711
|
-
height: this.rawVisualUserMedia?.height,
|
|
19712
|
-
videoWidth: this.rawVisualUserMedia?.videoWidth,
|
|
19713
|
-
videoHeight: this.rawVisualUserMedia?.videoHeight
|
|
19714
|
-
};
|
|
19443
|
+
handleConnectionFailure(params) {
|
|
19444
|
+
if (this.retryConnection(params)) return;
|
|
19445
|
+
this.failConnection(params);
|
|
19715
19446
|
}
|
|
19716
|
-
|
|
19717
|
-
|
|
19718
|
-
|
|
19719
|
-
|
|
19720
|
-
|
|
19721
|
-
|
|
19722
|
-
|
|
19723
|
-
|
|
19724
|
-
|
|
19725
|
-
|
|
19726
|
-
|
|
19727
|
-
|
|
19728
|
-
|
|
19729
|
-
|
|
19730
|
-
confirmedFrameNumber = 0;
|
|
19731
|
-
confirmedSampleNumber = 0;
|
|
19732
|
-
recorderElement;
|
|
19733
|
-
userMedia;
|
|
19734
|
-
userMediaTimeout;
|
|
19735
|
-
retryTimeout;
|
|
19736
|
-
reconnectTimeout;
|
|
19737
|
-
connectionTimeout;
|
|
19738
|
-
backpressureTimeout;
|
|
19739
|
-
stopTimeout;
|
|
19740
|
-
frameProgress;
|
|
19741
|
-
sampleProgress;
|
|
19742
|
-
canvas;
|
|
19743
|
-
recordingDimensions;
|
|
19744
|
-
ctx;
|
|
19745
|
-
userMediaLoaded;
|
|
19746
|
-
userMediaLoading = false;
|
|
19747
|
-
mediaRequestId = 0;
|
|
19748
|
-
cameraRecovery;
|
|
19749
|
-
resolveCameraRecovery;
|
|
19750
|
-
resumeAfterCameraRecovery = false;
|
|
19751
|
-
submitting = false;
|
|
19752
|
-
unloaded;
|
|
19753
|
-
stopTime;
|
|
19754
|
-
stream;
|
|
19755
|
-
streamBackpressured = false;
|
|
19756
|
-
resumeAfterDrain = false;
|
|
19757
|
-
mediaTransmissionStarted = false;
|
|
19758
|
-
recordingActive = false;
|
|
19759
|
-
recordingTransportFailed = false;
|
|
19760
|
-
connecting = false;
|
|
19761
|
-
connected = false;
|
|
19762
|
-
connectionFailed = false;
|
|
19763
|
-
reconnecting = false;
|
|
19764
|
-
reconnectAttempts = 0;
|
|
19765
|
-
blocking = false;
|
|
19766
|
-
built = false;
|
|
19767
|
-
key;
|
|
19768
|
-
waitingTime;
|
|
19769
|
-
connectingStartedAt;
|
|
19770
|
-
lastCloseEvent;
|
|
19771
|
-
lastSocketError;
|
|
19772
|
-
pingInterval;
|
|
19773
|
-
frame;
|
|
19774
|
-
recordingBuffer;
|
|
19775
|
-
facingMode;
|
|
19776
|
-
constructor(visuals, replay, options){
|
|
19777
|
-
super("Recorder", options);
|
|
19778
|
-
this.visuals = visuals;
|
|
19779
|
-
this.replay = replay;
|
|
19780
|
-
this.facingMode = options.video.facingMode;
|
|
19447
|
+
retryConnection(params) {
|
|
19448
|
+
if (!this.canRetryConnection()) return false;
|
|
19449
|
+
if (void 0 !== this.reconnectTimeout) return true;
|
|
19450
|
+
const delayMs = Math.min(1000 * 2 ** this.reconnectAttempts, 10000);
|
|
19451
|
+
this.reconnectAttempts++;
|
|
19452
|
+
this.connecting = false;
|
|
19453
|
+
this.clearConnectionTimeout();
|
|
19454
|
+
this.discardSocket();
|
|
19455
|
+
this.options.logger.debug(`Recorder: retrying WebSocket connection in ${delayMs}ms (cause ${params.cause}).`);
|
|
19456
|
+
this.reconnectTimeout = window.setTimeout(()=>{
|
|
19457
|
+
this.reconnectTimeout = void 0;
|
|
19458
|
+
if (this.shouldContinueReconnect()) this.initSocket();
|
|
19459
|
+
}, delayMs);
|
|
19460
|
+
return true;
|
|
19781
19461
|
}
|
|
19782
|
-
|
|
19783
|
-
return
|
|
19462
|
+
canRetryConnection() {
|
|
19463
|
+
return Boolean(this.reconnecting && this.userMediaLoaded && this.isOnline() && !this.unloaded && !this.blocking);
|
|
19784
19464
|
}
|
|
19785
|
-
|
|
19786
|
-
|
|
19787
|
-
this.stopPings();
|
|
19788
|
-
const err = error_createError({
|
|
19789
|
-
message: "Already disconnected",
|
|
19790
|
-
explanation: "Sorry, connection to the server has been destroyed. Please reload.",
|
|
19791
|
-
options: this.options
|
|
19792
|
-
});
|
|
19793
|
-
this.handleWriteFailure(err);
|
|
19794
|
-
} else if (this.streamBackpressured) {
|
|
19795
|
-
if (this.hasExceededWebSocketSendBuffer(buffer, opts)) this.failMediaTransmission(error_createError({
|
|
19796
|
-
message: "WebSocket send buffer limit exceeded",
|
|
19797
|
-
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.",
|
|
19798
|
-
options: this.options
|
|
19799
|
-
}));
|
|
19800
|
-
} else this.writeToStream(this.stream, buffer, opts);
|
|
19465
|
+
shouldContinueReconnect() {
|
|
19466
|
+
return this.reconnecting && !this.unloaded && !this.blocking;
|
|
19801
19467
|
}
|
|
19802
|
-
|
|
19803
|
-
|
|
19804
|
-
try {
|
|
19805
|
-
const canContinue = stream.write(buffer, ()=>{
|
|
19806
|
-
if (!onFlushedCallback) return;
|
|
19807
|
-
try {
|
|
19808
|
-
onFlushedCallback(opts);
|
|
19809
|
-
} catch (exc) {
|
|
19810
|
-
const err = error_createError({
|
|
19811
|
-
message: "Failed to write stream buffer",
|
|
19812
|
-
explanation: `stream.write() failed because of ${string_pretty(exc)}`,
|
|
19813
|
-
options: this.options,
|
|
19814
|
-
exc
|
|
19815
|
-
});
|
|
19816
|
-
this.emit("ERROR", {
|
|
19817
|
-
err
|
|
19818
|
-
});
|
|
19819
|
-
}
|
|
19820
|
-
});
|
|
19821
|
-
if (!canContinue) {
|
|
19822
|
-
this.streamBackpressured = true;
|
|
19823
|
-
this.startBackpressureTimeout();
|
|
19824
|
-
this.pauseForBackpressure();
|
|
19825
|
-
return false;
|
|
19826
|
-
}
|
|
19827
|
-
return true;
|
|
19828
|
-
} catch (exc) {
|
|
19829
|
-
const err = error_createError({
|
|
19830
|
-
message: "Failed writing to server",
|
|
19831
|
-
explanation: `stream.write() failed because of ${string_pretty(exc)}`,
|
|
19832
|
-
options: this.options,
|
|
19833
|
-
exc
|
|
19834
|
-
});
|
|
19835
|
-
this.handleWriteFailure(err);
|
|
19836
|
-
return false;
|
|
19837
|
-
}
|
|
19838
|
-
}
|
|
19839
|
-
flushPendingWrites(stream) {
|
|
19840
|
-
if (this.isStaleStream(stream)) return;
|
|
19841
|
-
this.streamBackpressured = false;
|
|
19842
|
-
this.clearBackpressureTimeout();
|
|
19843
|
-
let pendingWrite = this.pendingWrites.dequeue();
|
|
19844
|
-
while(pendingWrite){
|
|
19845
|
-
if (!this.writeToStream(stream, pendingWrite.buffer, pendingWrite.params)) return;
|
|
19846
|
-
pendingWrite = this.pendingWrites.dequeue();
|
|
19847
|
-
}
|
|
19848
|
-
this.resumeRecordingAfterDrain();
|
|
19849
|
-
}
|
|
19850
|
-
isStaleStream(stream) {
|
|
19851
|
-
return this.stream !== stream || stream.destroyed;
|
|
19852
|
-
}
|
|
19853
|
-
canPauseForBackpressure() {
|
|
19854
|
-
return Boolean(this.recordingActive && !this.userMedia?.isPaused() && this.loop?.isRunning());
|
|
19855
|
-
}
|
|
19856
|
-
pauseForBackpressure() {
|
|
19857
|
-
if (!this.canPauseForBackpressure()) return;
|
|
19858
|
-
this.resumeAfterDrain = true;
|
|
19859
|
-
this.loop.stop();
|
|
19860
|
-
}
|
|
19861
|
-
resumeRecordingAfterDrain() {
|
|
19862
|
-
if (!this.resumeAfterDrain) return;
|
|
19863
|
-
this.resumeAfterDrain = false;
|
|
19864
|
-
if (this.canResumeAfterDrain()) this.loop?.start();
|
|
19865
|
-
}
|
|
19866
|
-
canResumeAfterDrain() {
|
|
19867
|
-
return this.recordingActive && !this.userMedia?.isPaused();
|
|
19868
|
-
}
|
|
19869
|
-
handleWriteFailure(err) {
|
|
19870
|
-
if (this.mediaTransmissionStarted) return void this.failMediaTransmission(err);
|
|
19871
|
-
this.emit("ERROR", {
|
|
19872
|
-
err
|
|
19873
|
-
});
|
|
19468
|
+
isOnline() {
|
|
19469
|
+
return navigator.onLine;
|
|
19874
19470
|
}
|
|
19875
|
-
|
|
19876
|
-
if (this.
|
|
19877
|
-
this.
|
|
19878
|
-
this.recordingActive = false;
|
|
19879
|
-
this.mediaTransmissionStarted = false;
|
|
19880
|
-
this.streamBackpressured = false;
|
|
19881
|
-
this.resumeAfterDrain = false;
|
|
19882
|
-
this.pendingWrites.clear();
|
|
19883
|
-
this.connected = false;
|
|
19471
|
+
failConnection(params) {
|
|
19472
|
+
if (this.shouldIgnoreConnectionFailure()) return;
|
|
19473
|
+
this.connectionFailed = true;
|
|
19884
19474
|
this.connecting = false;
|
|
19885
19475
|
this.reconnecting = false;
|
|
19476
|
+
this.reconnectAttempts = 0;
|
|
19886
19477
|
this.clearReconnectTimeout();
|
|
19887
|
-
this.
|
|
19478
|
+
this.clearConnectionTimeout();
|
|
19479
|
+
const { url2Connect, cause } = params;
|
|
19480
|
+
const online = this.isOnline();
|
|
19481
|
+
const elapsedMs = this.connectingStartedAt ? Date.now() - this.connectingStartedAt : void 0;
|
|
19482
|
+
const closeEvent = this.lastCloseEvent;
|
|
19483
|
+
const socketError = this.lastSocketError;
|
|
19484
|
+
const contents = {
|
|
19485
|
+
cause,
|
|
19486
|
+
closeCode: closeEvent?.code,
|
|
19487
|
+
closeReason: closeEvent?.reason,
|
|
19488
|
+
elapsedMs,
|
|
19489
|
+
online,
|
|
19490
|
+
secureContext: globalThis.isSecureContext,
|
|
19491
|
+
socketReadyState: this.stream?.socket.readyState,
|
|
19492
|
+
timeoutMs: this.options.timeouts.connection,
|
|
19493
|
+
url: url2Connect,
|
|
19494
|
+
unloaded: this.unloaded,
|
|
19495
|
+
userMediaLoaded: this.userMediaLoaded,
|
|
19496
|
+
userMediaLoading: this.userMediaLoading,
|
|
19497
|
+
wasClean: closeEvent?.wasClean,
|
|
19498
|
+
socketError,
|
|
19499
|
+
blocking: this.blocking,
|
|
19500
|
+
visibilityState: document.visibilityState
|
|
19501
|
+
};
|
|
19502
|
+
const debugLine = string_summarize("Recorder: failConnection() diagnostic", contents);
|
|
19503
|
+
this.options.logger.debug(debugLine);
|
|
19504
|
+
let explanation;
|
|
19505
|
+
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.`;
|
|
19506
|
+
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.`;
|
|
19507
|
+
else {
|
|
19508
|
+
const closeSuffix = closeEvent ? ` (code ${closeEvent.code})` : "";
|
|
19509
|
+
explanation = `Connection to ${url2Connect} is closed${closeSuffix}. Please check your internet connection and try again. If the problem persists, contact us.`;
|
|
19510
|
+
}
|
|
19511
|
+
else explanation = "Your device appears to be offline. Please check your internet connection and try again.";
|
|
19888
19512
|
this.discardSocket();
|
|
19889
|
-
|
|
19890
|
-
|
|
19891
|
-
|
|
19892
|
-
|
|
19893
|
-
|
|
19513
|
+
const err = error_createError({
|
|
19514
|
+
message: "Unable to connect to the server",
|
|
19515
|
+
explanation,
|
|
19516
|
+
options: this.options,
|
|
19517
|
+
cause: contents,
|
|
19518
|
+
exc: socketError ? new Error("WebSocket connection error", {
|
|
19519
|
+
cause: socketError
|
|
19520
|
+
}) : void 0
|
|
19521
|
+
});
|
|
19894
19522
|
this.emit("ERROR", {
|
|
19895
19523
|
err
|
|
19896
19524
|
});
|
|
19897
19525
|
}
|
|
19898
|
-
|
|
19899
|
-
|
|
19900
|
-
|
|
19901
|
-
|
|
19902
|
-
|
|
19526
|
+
shouldIgnoreConnectionFailure() {
|
|
19527
|
+
return this.connectionFailed || this.connected || Boolean(this.unloaded) || this.validate();
|
|
19528
|
+
}
|
|
19529
|
+
initSocket(cb) {
|
|
19530
|
+
if (this.connected) {
|
|
19531
|
+
this.options.logger.debug("Recorder: already **connected**. Not going to initialize a new WebSocket connection.");
|
|
19532
|
+
cb?.();
|
|
19533
|
+
return;
|
|
19534
|
+
}
|
|
19535
|
+
if (this.connecting) return void this.options.logger.debug("Recorder: already **connecting**. Not going to initialize a new WebSocket connection.");
|
|
19536
|
+
this.clearReconnectTimeout();
|
|
19537
|
+
if (util_isAutomatedUserAgent()) {
|
|
19538
|
+
this.connecting = false;
|
|
19539
|
+
this.options.logger.debug("Recorder: skipping web socket connection for an automated crawler");
|
|
19540
|
+
return;
|
|
19541
|
+
}
|
|
19542
|
+
this.connecting = true;
|
|
19543
|
+
this.connectionFailed = false;
|
|
19544
|
+
this.connectingStartedAt = Date.now();
|
|
19545
|
+
this.lastCloseEvent = void 0;
|
|
19546
|
+
this.lastSocketError = void 0;
|
|
19547
|
+
this.emit("CONNECTING");
|
|
19548
|
+
let url2Connect;
|
|
19549
|
+
try {
|
|
19550
|
+
const socketUrlObj = new URL(this.options.socketUrl);
|
|
19551
|
+
socketUrlObj.searchParams.set(constants.WHITELIST_KEY_LABEL, this.options.whitelistKey);
|
|
19552
|
+
url2Connect = socketUrlObj.toString();
|
|
19553
|
+
} catch (exc) {
|
|
19554
|
+
this.connecting = this.connected = false;
|
|
19903
19555
|
const err = error_createError({
|
|
19904
|
-
message: "WebSocket
|
|
19905
|
-
explanation:
|
|
19906
|
-
options: this.options
|
|
19556
|
+
message: "Invalid WebSocket URL",
|
|
19557
|
+
explanation: `The configured socketUrl "${this.options.socketUrl}" is not a valid URL. Please check your videomail-client configuration.`,
|
|
19558
|
+
options: this.options,
|
|
19559
|
+
exc
|
|
19907
19560
|
});
|
|
19908
|
-
this.
|
|
19909
|
-
|
|
19910
|
-
}
|
|
19911
|
-
shouldStartBackpressureTimeout() {
|
|
19912
|
-
return void 0 === this.backpressureTimeout && this.mediaTransmissionStarted;
|
|
19913
|
-
}
|
|
19914
|
-
shouldIgnoreBackpressureTimeout() {
|
|
19915
|
-
return !this.streamBackpressured || this.recordingTransportFailed;
|
|
19916
|
-
}
|
|
19917
|
-
failInterruptedRecording() {
|
|
19918
|
-
const unconfirmedFrames = Math.max(0, this.framesCount - this.confirmedFrameNumber);
|
|
19919
|
-
const unconfirmedSamples = Math.max(0, this.samplesCount - this.confirmedSampleNumber);
|
|
19920
|
-
const err = error_createError({
|
|
19921
|
-
message: "Recording interrupted",
|
|
19922
|
-
explanation: "The server connection closed before this recording completed. It cannot be safely resumed, so it has been stopped. Please start a new recording.",
|
|
19923
|
-
options: this.options,
|
|
19924
|
-
cause: {
|
|
19925
|
-
unconfirmedFrames,
|
|
19926
|
-
unconfirmedSamples
|
|
19927
|
-
}
|
|
19928
|
-
});
|
|
19929
|
-
this.failMediaTransmission(err);
|
|
19930
|
-
}
|
|
19931
|
-
handleStreamDrain(stream) {
|
|
19932
|
-
this.options.logger.debug(`${PIPE_SYMBOL}Stream *drain* event emitted`);
|
|
19933
|
-
this.flushPendingWrites(stream);
|
|
19934
|
-
}
|
|
19935
|
-
canReconnectAfterStreamClose() {
|
|
19936
|
-
return Boolean(this.connected && this.userMediaLoaded);
|
|
19937
|
-
}
|
|
19938
|
-
shouldReportStreamClose() {
|
|
19939
|
-
return !this.connecting && !this.blocking;
|
|
19940
|
-
}
|
|
19941
|
-
handleStreamClose(url2Connect) {
|
|
19942
|
-
const debugLine = string_summarize(`${PIPE_SYMBOL}Stream has closed:`, {
|
|
19943
|
-
connecting: this.connecting,
|
|
19944
|
-
connected: this.connected,
|
|
19945
|
-
userMediaLoaded: this.userMediaLoaded,
|
|
19946
|
-
blocking: this.blocking
|
|
19947
|
-
});
|
|
19948
|
-
this.options.logger.debug(debugLine);
|
|
19949
|
-
const canReconnect = this.canReconnectAfterStreamClose();
|
|
19950
|
-
this.connected = false;
|
|
19951
|
-
this.stopPings();
|
|
19952
|
-
if (this.shouldIgnoreStreamClose()) return;
|
|
19953
|
-
if (this.mediaTransmissionStarted) this.failInterruptedRecording();
|
|
19954
|
-
else if (canReconnect) {
|
|
19955
|
-
this.pendingWrites.clear();
|
|
19956
|
-
this.streamBackpressured = false;
|
|
19957
|
-
this.resumeAfterDrain = false;
|
|
19958
|
-
this.reconnecting = true;
|
|
19959
|
-
this.initSocket();
|
|
19960
|
-
} else if (this.shouldReportStreamClose()) window.setTimeout(()=>{
|
|
19961
|
-
this.handleConnectionFailure({
|
|
19962
|
-
url2Connect,
|
|
19963
|
-
cause: "closed"
|
|
19561
|
+
this.emit("ERROR", {
|
|
19562
|
+
err
|
|
19964
19563
|
});
|
|
19965
|
-
}, 0);
|
|
19966
|
-
}
|
|
19967
|
-
shouldIgnoreStreamClose() {
|
|
19968
|
-
return this.recordingTransportFailed || Boolean(this.unloaded) || this.validate();
|
|
19969
|
-
}
|
|
19970
|
-
handleStreamError(url2Connect, err) {
|
|
19971
|
-
if ("u" > typeof Event && err instanceof Event) this.lastSocketError = error_getEventDetails(err);
|
|
19972
|
-
else this.lastSocketError = serializeError(err);
|
|
19973
|
-
const debugLine = string_summarize(`${PIPE_SYMBOL}Stream *error* event emitted:`, {
|
|
19974
|
-
error: serializeError(err)
|
|
19975
|
-
});
|
|
19976
|
-
this.options.logger.debug(debugLine);
|
|
19977
|
-
if (this.validate()) {
|
|
19978
|
-
this.connected = false;
|
|
19979
|
-
this.stopPings();
|
|
19980
|
-
this.discardSocket();
|
|
19981
19564
|
return;
|
|
19982
19565
|
}
|
|
19983
|
-
|
|
19984
|
-
|
|
19985
|
-
|
|
19986
|
-
|
|
19987
|
-
|
|
19988
|
-
|
|
19989
|
-
|
|
19990
|
-
|
|
19991
|
-
|
|
19992
|
-
|
|
19993
|
-
|
|
19994
|
-
|
|
19566
|
+
this.options.logger.debug(`Recorder: initializing web socket stream to ${url2Connect}`);
|
|
19567
|
+
let nativeSocket;
|
|
19568
|
+
try {
|
|
19569
|
+
nativeSocket = new WebSocket(url2Connect);
|
|
19570
|
+
nativeSocket.addEventListener("close", (event)=>{
|
|
19571
|
+
this.lastCloseEvent = {
|
|
19572
|
+
code: event.code,
|
|
19573
|
+
reason: event.reason,
|
|
19574
|
+
wasClean: event.wasClean
|
|
19575
|
+
};
|
|
19576
|
+
});
|
|
19577
|
+
nativeSocket.addEventListener("error", (event)=>{
|
|
19578
|
+
this.lastSocketError = error_getEventDetails(event);
|
|
19579
|
+
});
|
|
19580
|
+
} catch (exc) {
|
|
19581
|
+
this.connecting = this.connected = false;
|
|
19582
|
+
const diagnostic = error_getWebSocketDiagnostic(url2Connect);
|
|
19583
|
+
const message = diagnostic.looksAutomated ? "Automated crawler: WebSocket not supported in this environment" : `Failed to construct WebSocket to ${url2Connect}`;
|
|
19584
|
+
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}`;
|
|
19585
|
+
const err = error_createError({
|
|
19586
|
+
message,
|
|
19587
|
+
explanation,
|
|
19995
19588
|
options: this.options,
|
|
19996
|
-
exc: streamError
|
|
19997
|
-
})
|
|
19998
|
-
});
|
|
19999
|
-
}
|
|
20000
|
-
sendPings() {
|
|
20001
|
-
this.stopPings();
|
|
20002
|
-
this.pingInterval = window.setInterval(()=>{
|
|
20003
|
-
this.options.logger.debug("Recorder: pinging...");
|
|
20004
|
-
this.writeStream(Buffer.from(""));
|
|
20005
|
-
}, this.options.timeouts.pingInterval);
|
|
20006
|
-
}
|
|
20007
|
-
stopPings() {
|
|
20008
|
-
if (void 0 === this.pingInterval) return;
|
|
20009
|
-
this.options.logger.debug("Recorder: stopping pings...");
|
|
20010
|
-
window.clearInterval(this.pingInterval);
|
|
20011
|
-
this.pingInterval = void 0;
|
|
20012
|
-
}
|
|
20013
|
-
onAudioSample(audioSample) {
|
|
20014
|
-
if (this.recordingTransportFailed || this.cameraRecovery) return;
|
|
20015
|
-
const sampleRate = this.userMedia?.getAudioSampleRate();
|
|
20016
|
-
if (sampleRate && sampleRate > 0) this.recordingAudioSampleRate ??= sampleRate;
|
|
20017
|
-
this.samplesCount++;
|
|
20018
|
-
this.mediaTransmissionStarted = true;
|
|
20019
|
-
const audioBuffer = audioSample.toBuffer();
|
|
20020
|
-
this.writeStream(audioBuffer);
|
|
20021
|
-
}
|
|
20022
|
-
show() {
|
|
20023
|
-
html_showElement(this.recorderElement);
|
|
20024
|
-
}
|
|
20025
|
-
onUserMediaReady(params) {
|
|
20026
|
-
try {
|
|
20027
|
-
this.options.logger.debug(`Recorder: onUserMediaReady(${params ? string_pretty(params) : ""})`);
|
|
20028
|
-
const switchingFacingMode = params?.switchingFacingMode;
|
|
20029
|
-
if (params?.recovering) {
|
|
20030
|
-
if (!this.userMedia?.hasLiveStream()) return void this.failCameraRecovery(new Error("The restored camera has no live tracks."));
|
|
20031
|
-
if (void 0 !== this.recordingAudioSampleRate && this.userMedia.getAudioSampleRate() !== this.recordingAudioSampleRate) return void this.failCameraRecovery(new Error("The audio sample rate changed during recovery."));
|
|
20032
|
-
}
|
|
20033
|
-
this.userMediaLoading = this.blocking = this.unloaded = this.submitting = false;
|
|
20034
|
-
this.userMediaLoaded = true;
|
|
20035
|
-
this.clearUserMediaTimeout();
|
|
20036
|
-
if (!switchingFacingMode && !params?.recovering) this.loop = this.createLoop();
|
|
20037
|
-
this.show();
|
|
20038
|
-
if (params?.recordWhenReady) this.record();
|
|
20039
|
-
this.emit("USER_MEDIA_READY", {
|
|
20040
|
-
switchingFacingMode: params?.switchingFacingMode,
|
|
20041
|
-
recovering: params?.recovering,
|
|
20042
|
-
paused: this.isPaused(),
|
|
20043
|
-
recordWhenReady: params?.recordWhenReady
|
|
20044
|
-
});
|
|
20045
|
-
if (params?.recovering) this.finishCameraRecovery(true);
|
|
20046
|
-
} catch (exc) {
|
|
20047
|
-
if (params?.recovering) this.failCameraRecovery(exc);
|
|
20048
|
-
else this.emit("ERROR", {
|
|
20049
19589
|
exc
|
|
20050
19590
|
});
|
|
20051
|
-
|
|
20052
|
-
|
|
20053
|
-
clearRetryTimeout() {
|
|
20054
|
-
if (void 0 === this.retryTimeout) return;
|
|
20055
|
-
this.options.logger.debug("Recorder: clearRetryTimeout()");
|
|
20056
|
-
window.clearTimeout(this.retryTimeout);
|
|
20057
|
-
this.retryTimeout = void 0;
|
|
20058
|
-
}
|
|
20059
|
-
clearConnectionTimeout() {
|
|
20060
|
-
if (void 0 === this.connectionTimeout) return;
|
|
20061
|
-
this.options.logger.debug("Recorder: clearConnectionTimeout()");
|
|
20062
|
-
window.clearTimeout(this.connectionTimeout);
|
|
20063
|
-
this.connectionTimeout = void 0;
|
|
20064
|
-
}
|
|
20065
|
-
clearBackpressureTimeout() {
|
|
20066
|
-
if (void 0 === this.backpressureTimeout) return;
|
|
20067
|
-
window.clearTimeout(this.backpressureTimeout);
|
|
20068
|
-
this.backpressureTimeout = void 0;
|
|
20069
|
-
}
|
|
20070
|
-
clearReconnectTimeout() {
|
|
20071
|
-
if (void 0 === this.reconnectTimeout) return;
|
|
20072
|
-
this.options.logger.debug("Recorder: clearReconnectTimeout()");
|
|
20073
|
-
window.clearTimeout(this.reconnectTimeout);
|
|
20074
|
-
this.reconnectTimeout = void 0;
|
|
20075
|
-
}
|
|
20076
|
-
handleConnectionFailure(params) {
|
|
20077
|
-
if (this.retryConnection(params)) return;
|
|
20078
|
-
this.failConnection(params);
|
|
20079
|
-
}
|
|
20080
|
-
retryConnection(params) {
|
|
20081
|
-
if (!this.canRetryConnection()) return false;
|
|
20082
|
-
if (void 0 !== this.reconnectTimeout) return true;
|
|
20083
|
-
const delayMs = Math.min(1000 * 2 ** this.reconnectAttempts, 10000);
|
|
20084
|
-
this.reconnectAttempts++;
|
|
20085
|
-
this.connecting = false;
|
|
20086
|
-
this.clearConnectionTimeout();
|
|
20087
|
-
this.discardSocket();
|
|
20088
|
-
this.options.logger.debug(`Recorder: retrying WebSocket connection in ${delayMs}ms (cause ${params.cause}).`);
|
|
20089
|
-
this.reconnectTimeout = window.setTimeout(()=>{
|
|
20090
|
-
this.reconnectTimeout = void 0;
|
|
20091
|
-
if (this.shouldContinueReconnect()) this.initSocket();
|
|
20092
|
-
}, delayMs);
|
|
20093
|
-
return true;
|
|
20094
|
-
}
|
|
20095
|
-
canRetryConnection() {
|
|
20096
|
-
return Boolean(this.reconnecting && this.userMediaLoaded && this.isOnline() && !this.unloaded && !this.blocking);
|
|
20097
|
-
}
|
|
20098
|
-
shouldContinueReconnect() {
|
|
20099
|
-
return this.reconnecting && !this.unloaded && !this.blocking;
|
|
20100
|
-
}
|
|
20101
|
-
discardSocket() {
|
|
20102
|
-
this.options.logger.debug("Recorder: discarding socket stream ...");
|
|
20103
|
-
this.clearBackpressureTimeout();
|
|
20104
|
-
this.pendingWrites.clear();
|
|
20105
|
-
this.streamBackpressured = false;
|
|
20106
|
-
this.resumeAfterDrain = false;
|
|
20107
|
-
const stream = this.stream;
|
|
20108
|
-
this.stream = void 0;
|
|
20109
|
-
if (stream && !stream.destroyed) stream.destroy();
|
|
20110
|
-
}
|
|
20111
|
-
isOnline() {
|
|
20112
|
-
return navigator.onLine;
|
|
20113
|
-
}
|
|
20114
|
-
failConnection(params) {
|
|
20115
|
-
if (this.shouldIgnoreConnectionFailure()) return;
|
|
20116
|
-
this.connectionFailed = true;
|
|
20117
|
-
this.connecting = false;
|
|
20118
|
-
this.reconnecting = false;
|
|
20119
|
-
this.reconnectAttempts = 0;
|
|
20120
|
-
this.clearReconnectTimeout();
|
|
20121
|
-
this.clearConnectionTimeout();
|
|
20122
|
-
const { url2Connect, cause } = params;
|
|
20123
|
-
const online = this.isOnline();
|
|
20124
|
-
const elapsedMs = this.connectingStartedAt ? Date.now() - this.connectingStartedAt : void 0;
|
|
20125
|
-
const closeEvent = this.lastCloseEvent;
|
|
20126
|
-
const socketError = this.lastSocketError;
|
|
20127
|
-
const contents = {
|
|
20128
|
-
cause,
|
|
20129
|
-
closeCode: closeEvent?.code,
|
|
20130
|
-
closeReason: closeEvent?.reason,
|
|
20131
|
-
elapsedMs,
|
|
20132
|
-
online,
|
|
20133
|
-
secureContext: globalThis.isSecureContext,
|
|
20134
|
-
socketReadyState: this.stream?.socket.readyState,
|
|
20135
|
-
timeoutMs: this.options.timeouts.connection,
|
|
20136
|
-
url: url2Connect,
|
|
20137
|
-
unloaded: this.unloaded,
|
|
20138
|
-
userMediaLoaded: this.userMediaLoaded,
|
|
20139
|
-
userMediaLoading: this.userMediaLoading,
|
|
20140
|
-
wasClean: closeEvent?.wasClean,
|
|
20141
|
-
socketError,
|
|
20142
|
-
blocking: this.blocking,
|
|
20143
|
-
visibilityState: document.visibilityState
|
|
20144
|
-
};
|
|
20145
|
-
const debugLine = string_summarize("Recorder: failConnection() diagnostic", contents);
|
|
20146
|
-
this.options.logger.debug(debugLine);
|
|
20147
|
-
let explanation;
|
|
20148
|
-
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.`;
|
|
20149
|
-
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.`;
|
|
20150
|
-
else {
|
|
20151
|
-
const closeSuffix = closeEvent ? ` (code ${closeEvent.code})` : "";
|
|
20152
|
-
explanation = `Connection to ${url2Connect} is closed${closeSuffix}. Please check your internet connection and try again. If the problem persists, contact us.`;
|
|
20153
|
-
}
|
|
20154
|
-
else explanation = "Your device appears to be offline. Please check your internet connection and try again.";
|
|
20155
|
-
this.discardSocket();
|
|
20156
|
-
const err = error_createError({
|
|
20157
|
-
message: "Unable to connect to the server",
|
|
20158
|
-
explanation,
|
|
20159
|
-
options: this.options,
|
|
20160
|
-
cause: contents,
|
|
20161
|
-
exc: socketError ? new Error("WebSocket connection error", {
|
|
20162
|
-
cause: socketError
|
|
20163
|
-
}) : void 0
|
|
20164
|
-
});
|
|
20165
|
-
this.emit("ERROR", {
|
|
20166
|
-
err
|
|
20167
|
-
});
|
|
20168
|
-
}
|
|
20169
|
-
shouldIgnoreConnectionFailure() {
|
|
20170
|
-
return this.connectionFailed || this.connected || Boolean(this.unloaded) || this.validate();
|
|
20171
|
-
}
|
|
20172
|
-
calculateFrameProgress() {
|
|
20173
|
-
return `${(this.confirmedFrameNumber / (this.framesCount || 1) * 100).toFixed(2)}%`;
|
|
20174
|
-
}
|
|
20175
|
-
calculateSampleProgress() {
|
|
20176
|
-
return `${(this.confirmedSampleNumber / (this.samplesCount || 1) * 100).toFixed(2)}%`;
|
|
20177
|
-
}
|
|
20178
|
-
updateOverallProgress() {
|
|
20179
|
-
this.frameProgress = this.calculateFrameProgress();
|
|
20180
|
-
if (isAudioEnabled(this.options)) this.sampleProgress = this.calculateSampleProgress();
|
|
20181
|
-
this.emit("PROGRESS", {
|
|
20182
|
-
frameProgress: this.frameProgress,
|
|
20183
|
-
sampleProgress: this.sampleProgress
|
|
20184
|
-
});
|
|
20185
|
-
}
|
|
20186
|
-
updateFrameProgress(args) {
|
|
20187
|
-
if (args) {
|
|
20188
|
-
if (!args.frame) throw error_createError({
|
|
20189
|
-
message: "The frame number is missing",
|
|
20190
|
-
options: this.options
|
|
20191
|
-
});
|
|
20192
|
-
} else throw error_createError({
|
|
20193
|
-
message: "Arguments are missing for updating the frame progress",
|
|
20194
|
-
options: this.options
|
|
20195
|
-
});
|
|
20196
|
-
this.confirmedFrameNumber = args.frame;
|
|
20197
|
-
this.frameProgress = this.calculateFrameProgress();
|
|
20198
|
-
this.updateOverallProgress();
|
|
20199
|
-
}
|
|
20200
|
-
updateSampleProgress(args) {
|
|
20201
|
-
if (args) {
|
|
20202
|
-
if (!args.sample) throw error_createError({
|
|
20203
|
-
message: "The audio sample number is missing",
|
|
20204
|
-
options: this.options
|
|
19591
|
+
this.emit("ERROR", {
|
|
19592
|
+
err
|
|
20205
19593
|
});
|
|
20206
|
-
} else throw error_createError({
|
|
20207
|
-
message: "Arguments are missing for updating the audio sample progress",
|
|
20208
|
-
options: this.options
|
|
20209
|
-
});
|
|
20210
|
-
this.confirmedSampleNumber = args.sample;
|
|
20211
|
-
this.sampleProgress = this.calculateSampleProgress();
|
|
20212
|
-
this.updateOverallProgress();
|
|
20213
|
-
}
|
|
20214
|
-
preview(args) {
|
|
20215
|
-
if (!args) throw error_createError({
|
|
20216
|
-
message: "Preview arguments are missing.",
|
|
20217
|
-
options: this.options
|
|
20218
|
-
});
|
|
20219
|
-
const hasAudio = this.samplesCount > 0;
|
|
20220
|
-
this.mediaTransmissionStarted = false;
|
|
20221
|
-
this.recordingActive = false;
|
|
20222
|
-
this.recordingTransportFailed = false;
|
|
20223
|
-
this.confirmedFrameNumber = this.confirmedSampleNumber = this.samplesCount = this.framesCount = 0;
|
|
20224
|
-
this.sampleProgress = this.frameProgress = void 0;
|
|
20225
|
-
this.key = args.key;
|
|
20226
|
-
if (args.mp4) this.replay.setMp4Source(`${args.mp4 + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.mp4`, true);
|
|
20227
|
-
if (args.webm) this.replay.setWebMSource(`${args.webm + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.webm`, true);
|
|
20228
|
-
this.hide();
|
|
20229
|
-
const widthDimension = this.getRecorderWidth(true);
|
|
20230
|
-
const heightDimension = this.getRecorderHeight(true);
|
|
20231
|
-
const duration = args.duration ?? -1;
|
|
20232
|
-
this.emit("PREVIEW", {
|
|
20233
|
-
key: this.key,
|
|
20234
|
-
width: widthDimension?.value,
|
|
20235
|
-
height: heightDimension?.value,
|
|
20236
|
-
hasAudio,
|
|
20237
|
-
duration
|
|
20238
|
-
});
|
|
20239
|
-
if (this.stopTime) this.waitingTime = Date.now() - this.stopTime;
|
|
20240
|
-
this.recordingStats ??= {};
|
|
20241
|
-
this.recordingStats.waitingTime = this.waitingTime;
|
|
20242
|
-
}
|
|
20243
|
-
initSocket(cb) {
|
|
20244
|
-
if (this.connected) {
|
|
20245
|
-
this.options.logger.debug("Recorder: already **connected**. Not going to initialize a new WebSocket connection.");
|
|
20246
|
-
cb?.();
|
|
20247
|
-
return;
|
|
20248
|
-
}
|
|
20249
|
-
if (this.connecting) return void this.options.logger.debug("Recorder: already **connecting**. Not going to initialize a new WebSocket connection.");
|
|
20250
|
-
this.clearReconnectTimeout();
|
|
20251
|
-
if (util_isAutomatedUserAgent()) {
|
|
20252
|
-
this.connecting = false;
|
|
20253
|
-
this.options.logger.debug("Recorder: skipping web socket connection for an automated crawler");
|
|
20254
19594
|
return;
|
|
20255
19595
|
}
|
|
20256
|
-
this.connecting = true;
|
|
20257
|
-
this.connectionFailed = false;
|
|
20258
|
-
this.connectingStartedAt = Date.now();
|
|
20259
|
-
this.lastCloseEvent = void 0;
|
|
20260
|
-
this.lastSocketError = void 0;
|
|
20261
|
-
this.emit("CONNECTING");
|
|
20262
|
-
let url2Connect;
|
|
20263
19596
|
try {
|
|
20264
|
-
|
|
20265
|
-
socketUrlObj.searchParams.set(constants.WHITELIST_KEY_LABEL, this.options.whitelistKey);
|
|
20266
|
-
url2Connect = socketUrlObj.toString();
|
|
19597
|
+
this.stream = stream_default()(nativeSocket);
|
|
20267
19598
|
} catch (exc) {
|
|
20268
19599
|
this.connecting = this.connected = false;
|
|
20269
19600
|
const err = error_createError({
|
|
20270
|
-
message:
|
|
20271
|
-
explanation:
|
|
20272
|
-
options: this.options,
|
|
20273
|
-
exc
|
|
20274
|
-
});
|
|
20275
|
-
this.emit("ERROR", {
|
|
20276
|
-
err
|
|
20277
|
-
});
|
|
20278
|
-
return;
|
|
20279
|
-
}
|
|
20280
|
-
this.options.logger.debug(`Recorder: initializing web socket stream to ${url2Connect}`);
|
|
20281
|
-
let nativeSocket;
|
|
20282
|
-
try {
|
|
20283
|
-
nativeSocket = new WebSocket(url2Connect);
|
|
20284
|
-
nativeSocket.addEventListener("close", (event)=>{
|
|
20285
|
-
this.lastCloseEvent = {
|
|
20286
|
-
code: event.code,
|
|
20287
|
-
reason: event.reason,
|
|
20288
|
-
wasClean: event.wasClean
|
|
20289
|
-
};
|
|
20290
|
-
});
|
|
20291
|
-
nativeSocket.addEventListener("error", (event)=>{
|
|
20292
|
-
this.lastSocketError = error_getEventDetails(event);
|
|
20293
|
-
});
|
|
20294
|
-
} catch (exc) {
|
|
20295
|
-
this.connecting = this.connected = false;
|
|
20296
|
-
const diagnostic = error_getWebSocketDiagnostic(url2Connect);
|
|
20297
|
-
const message = diagnostic.looksAutomated ? "Automated crawler: WebSocket not supported in this environment" : `Failed to construct WebSocket to ${url2Connect}`;
|
|
20298
|
-
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}`;
|
|
20299
|
-
const err = error_createError({
|
|
20300
|
-
message,
|
|
20301
|
-
explanation,
|
|
20302
|
-
options: this.options,
|
|
20303
|
-
exc
|
|
20304
|
-
});
|
|
20305
|
-
this.emit("ERROR", {
|
|
20306
|
-
err
|
|
20307
|
-
});
|
|
20308
|
-
return;
|
|
20309
|
-
}
|
|
20310
|
-
try {
|
|
20311
|
-
this.stream = stream_default()(nativeSocket);
|
|
20312
|
-
} catch (exc) {
|
|
20313
|
-
this.connecting = this.connected = false;
|
|
20314
|
-
const err = error_createError({
|
|
20315
|
-
message: `Failed to create a stream to ${url2Connect}`,
|
|
20316
|
-
explanation: "Please check your connection and try again. If the problem persists, contact us.",
|
|
19601
|
+
message: `Failed to create a stream to ${url2Connect}`,
|
|
19602
|
+
explanation: "Please check your connection and try again. If the problem persists, contact us.",
|
|
20317
19603
|
options: this.options,
|
|
20318
19604
|
exc
|
|
20319
19605
|
});
|
|
@@ -20407,6 +19693,81 @@
|
|
|
20407
19693
|
});
|
|
20408
19694
|
}
|
|
20409
19695
|
}
|
|
19696
|
+
writeCommand(command, args, cb) {
|
|
19697
|
+
if (this.connected) {
|
|
19698
|
+
if (this.stream) {
|
|
19699
|
+
if (args) this.options.logger.debug(`$ ${command} with ${string_pretty(args)}`);
|
|
19700
|
+
else this.options.logger.debug(`$ ${command}`);
|
|
19701
|
+
const commandObj = {
|
|
19702
|
+
command,
|
|
19703
|
+
args
|
|
19704
|
+
};
|
|
19705
|
+
this.writeStream(Buffer.from(JSON.stringify(commandObj)), cb ? {
|
|
19706
|
+
onFlushedCallback: ()=>{
|
|
19707
|
+
queueMicrotask(cb);
|
|
19708
|
+
}
|
|
19709
|
+
} : void 0);
|
|
19710
|
+
}
|
|
19711
|
+
} else {
|
|
19712
|
+
this.options.logger.debug(`Reconnecting for the command ${command} …`);
|
|
19713
|
+
this.initSocket(()=>{
|
|
19714
|
+
this.writeCommand(command, args, cb);
|
|
19715
|
+
});
|
|
19716
|
+
}
|
|
19717
|
+
}
|
|
19718
|
+
}
|
|
19719
|
+
const connection = RecorderConnection;
|
|
19720
|
+
function createUserMediaConstraints({ options, facingMode, limitedWidth, userMediaParams }) {
|
|
19721
|
+
const video = {
|
|
19722
|
+
frameRate: {
|
|
19723
|
+
ideal: options.video.fps
|
|
19724
|
+
}
|
|
19725
|
+
};
|
|
19726
|
+
if (userMediaParams?.switchingFacingMode || userMediaParams?.recovering) video.facingMode = userMediaParams.switchingFacingMode || facingMode;
|
|
19727
|
+
if (options.video.width) video.width = {
|
|
19728
|
+
ideal: options.video.width
|
|
19729
|
+
};
|
|
19730
|
+
else if (limitedWidth?.value) video.width = {
|
|
19731
|
+
ideal: limitedWidth.value
|
|
19732
|
+
};
|
|
19733
|
+
if (options.video.height) video.height = {
|
|
19734
|
+
ideal: options.video.height
|
|
19735
|
+
};
|
|
19736
|
+
return {
|
|
19737
|
+
audio: isAudioEnabled(options),
|
|
19738
|
+
video
|
|
19739
|
+
};
|
|
19740
|
+
}
|
|
19741
|
+
const mediaConstraints = createUserMediaConstraints;
|
|
19742
|
+
class RecorderMedia extends connection {
|
|
19743
|
+
visuals;
|
|
19744
|
+
recorderElement;
|
|
19745
|
+
userMedia;
|
|
19746
|
+
userMediaTimeout;
|
|
19747
|
+
retryTimeout;
|
|
19748
|
+
stopTimeout;
|
|
19749
|
+
mediaRequestId = 0;
|
|
19750
|
+
cameraRecovery;
|
|
19751
|
+
resolveCameraRecovery;
|
|
19752
|
+
resumeAfterCameraRecovery = false;
|
|
19753
|
+
submitting = false;
|
|
19754
|
+
built = false;
|
|
19755
|
+
key;
|
|
19756
|
+
facingMode;
|
|
19757
|
+
constructor(visuals, options){
|
|
19758
|
+
super(options);
|
|
19759
|
+
this.visuals = visuals;
|
|
19760
|
+
this.facingMode = options.video.facingMode;
|
|
19761
|
+
}
|
|
19762
|
+
show() {
|
|
19763
|
+
html_showElement(this.recorderElement);
|
|
19764
|
+
}
|
|
19765
|
+
clearRetryTimeout() {
|
|
19766
|
+
if (void 0 === this.retryTimeout) return;
|
|
19767
|
+
this.options.logger.debug("Recorder: clearRetryTimeout()");
|
|
19768
|
+
window.clearTimeout(this.retryTimeout);
|
|
19769
|
+
this.retryTimeout = void 0;
|
|
19770
|
+
}
|
|
20410
19771
|
showUserMedia() {
|
|
20411
19772
|
if (!this.connected) return false;
|
|
20412
19773
|
const hidden = this.isHidden();
|
|
@@ -20491,425 +19852,1372 @@
|
|
|
20491
19852
|
this.emit("ASKING_WEBCAM_PERMISSION");
|
|
20492
19853
|
this.userMediaLoading = true;
|
|
20493
19854
|
const requestId = ++this.mediaRequestId;
|
|
20494
|
-
const
|
|
20495
|
-
|
|
20496
|
-
|
|
20497
|
-
|
|
19855
|
+
const limitedWidth = this.options.video.width ? void 0 : this.visuals.limitWidth();
|
|
19856
|
+
const constraints = mediaConstraints({
|
|
19857
|
+
facingMode: this.facingMode,
|
|
19858
|
+
limitedWidth,
|
|
19859
|
+
options: this.options,
|
|
19860
|
+
userMediaParams: params
|
|
19861
|
+
});
|
|
19862
|
+
this.options.logger.debug(string_summarize("Recorder: our webcam constraints are:", constraints));
|
|
19863
|
+
this.options.logger.debug(string_summarize("Recorder: available webcam constraints are:", navigator.mediaDevices.getSupportedConstraints()));
|
|
19864
|
+
const streamPromise = navigator.mediaDevices.getUserMedia(constraints);
|
|
19865
|
+
streamPromise.then((localStream)=>{
|
|
19866
|
+
if (this.unloaded || requestId !== this.mediaRequestId) return void localStream.getTracks().forEach((track)=>{
|
|
19867
|
+
track.stop();
|
|
19868
|
+
});
|
|
19869
|
+
this.getUserMediaCallback(localStream, params, requestId);
|
|
19870
|
+
}).catch((reason)=>{
|
|
19871
|
+
if (!this.unloaded && requestId === this.mediaRequestId) this.userMediaErrorCallback(reason, constraints);
|
|
19872
|
+
});
|
|
19873
|
+
}
|
|
19874
|
+
loadUserMedia(params) {
|
|
19875
|
+
if (this.userMediaLoaded) {
|
|
19876
|
+
this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already loaded");
|
|
19877
|
+
this.onUserMediaReady(params);
|
|
19878
|
+
return;
|
|
19879
|
+
} else if (this.userMediaLoading) return void this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already asking for permission");
|
|
19880
|
+
this.options.logger.debug(`Recorder: loadUserMedia(${params ? string_pretty(params) : ""})`);
|
|
19881
|
+
this.emit("LOADING_USER_MEDIA");
|
|
19882
|
+
try {
|
|
19883
|
+
if (!navigator.mediaDevices) throw new Error("No media devices are available.");
|
|
19884
|
+
this.userMediaTimeout = window.setTimeout(()=>{
|
|
19885
|
+
if (!this.userMediaLoaded) {
|
|
19886
|
+
this.userMediaLoading = false;
|
|
19887
|
+
const err = util_getBrowser(this.options).getNoAccessIssue();
|
|
19888
|
+
this.userMediaErrorCallback(err, {
|
|
19889
|
+
audio: isAudioEnabled(this.options)
|
|
19890
|
+
});
|
|
19891
|
+
}
|
|
19892
|
+
}, this.options.timeouts.userMedia);
|
|
19893
|
+
this.userMediaLoading = true;
|
|
19894
|
+
this.loadGenuineUserMedia(params);
|
|
19895
|
+
} catch (exc) {
|
|
19896
|
+
this.clearUserMediaTimeout();
|
|
19897
|
+
this.options.logger.debug("Recorder: failed to load genuine user media");
|
|
19898
|
+
this.userMediaLoading = false;
|
|
19899
|
+
if (this.cameraRecovery) return void this.failCameraRecovery(exc);
|
|
19900
|
+
const errorListeners = this.getListeners("ERROR");
|
|
19901
|
+
if (errorListeners?.length) this.emit("ERROR", {
|
|
19902
|
+
exc
|
|
19903
|
+
});
|
|
19904
|
+
else {
|
|
19905
|
+
this.options.logger.debug("Recorder: no error listeners attached but throwing exception further");
|
|
19906
|
+
throw exc;
|
|
19907
|
+
}
|
|
19908
|
+
}
|
|
19909
|
+
}
|
|
19910
|
+
isNotifying() {
|
|
19911
|
+
return this.visuals.isNotifying();
|
|
19912
|
+
}
|
|
19913
|
+
isHidden() {
|
|
19914
|
+
return !this.recorderElement || html_isHidden(this.recorderElement);
|
|
19915
|
+
}
|
|
19916
|
+
clearUserMediaTimeout() {
|
|
19917
|
+
if (void 0 !== this.userMediaTimeout) {
|
|
19918
|
+
this.options.logger.debug("Recorder: clearUserMediaTimeout()");
|
|
19919
|
+
window.clearTimeout(this.userMediaTimeout);
|
|
19920
|
+
this.userMediaTimeout = void 0;
|
|
19921
|
+
}
|
|
19922
|
+
}
|
|
19923
|
+
clearStopTimeout() {
|
|
19924
|
+
if (void 0 === this.stopTimeout) return;
|
|
19925
|
+
window.clearTimeout(this.stopTimeout);
|
|
19926
|
+
this.stopTimeout = void 0;
|
|
19927
|
+
}
|
|
19928
|
+
isReady() {
|
|
19929
|
+
return this.userMedia?.isReady();
|
|
19930
|
+
}
|
|
19931
|
+
finishCameraRecovery(restored) {
|
|
19932
|
+
this.clearUserMediaTimeout();
|
|
19933
|
+
const resolve = this.resolveCameraRecovery;
|
|
19934
|
+
const resumeRequested = restored && this.resumeAfterCameraRecovery;
|
|
19935
|
+
this.cameraRecovery = void 0;
|
|
19936
|
+
this.resolveCameraRecovery = void 0;
|
|
19937
|
+
this.resumeAfterCameraRecovery = false;
|
|
19938
|
+
resolve?.(restored);
|
|
19939
|
+
if (resumeRequested && this.isPaused()) this.resume();
|
|
19940
|
+
}
|
|
19941
|
+
cancelCameraRecovery() {
|
|
19942
|
+
if (!this.cameraRecovery) return;
|
|
19943
|
+
this.mediaRequestId++;
|
|
19944
|
+
this.userMediaLoading = false;
|
|
19945
|
+
this.userMediaLoaded = false;
|
|
19946
|
+
this.clearUserMediaTimeout();
|
|
19947
|
+
this.userMedia?.stop(void 0, {
|
|
19948
|
+
preserveRecordingState: true
|
|
19949
|
+
});
|
|
19950
|
+
this.finishCameraRecovery(false);
|
|
19951
|
+
}
|
|
19952
|
+
failCameraRecovery(exc) {
|
|
19953
|
+
this.cancelCameraRecovery();
|
|
19954
|
+
const err = error_createError({
|
|
19955
|
+
message: "Camera recovery failed",
|
|
19956
|
+
explanation: "Your recording is still paused. Check camera permissions, then try Resume again or stop to keep the recorded part.",
|
|
19957
|
+
exc,
|
|
19958
|
+
options: this.options
|
|
19959
|
+
});
|
|
19960
|
+
this.emit("CAMERA_RECOVERY_FAILED", {
|
|
19961
|
+
err
|
|
19962
|
+
});
|
|
19963
|
+
}
|
|
19964
|
+
recoverCamera() {
|
|
19965
|
+
if (this.cameraRecovery) return this.cameraRecovery;
|
|
19966
|
+
if (!this.built || this.unloaded || !this.userMedia || this.userMediaLoading || this.submitting || this.key || void 0 !== this.stopTimeout || this.recordingTransportFailed) return Promise.resolve(false);
|
|
19967
|
+
const liveStream = this.userMedia.hasLiveStream();
|
|
19968
|
+
if (liveStream && this.isRecording()) return Promise.resolve(true);
|
|
19969
|
+
if (!liveStream && this.recordingActive && !this.isPaused()) this.pause();
|
|
19970
|
+
const recovery = new Promise((resolve)=>{
|
|
19971
|
+
this.resolveCameraRecovery = resolve;
|
|
19972
|
+
});
|
|
19973
|
+
this.cameraRecovery = recovery;
|
|
19974
|
+
this.emit("CAMERA_RECOVERING");
|
|
19975
|
+
if (liveStream) {
|
|
19976
|
+
this.userMediaTimeout = window.setTimeout(()=>{
|
|
19977
|
+
this.failCameraRecovery(new Error("Camera playback recovery timed out."));
|
|
19978
|
+
}, this.options.timeouts.userMedia);
|
|
19979
|
+
this.userMedia.restorePlayback().then(()=>{
|
|
19980
|
+
if (this.cameraRecovery === recovery) this.finishCameraRecovery(true);
|
|
19981
|
+
}).catch((exc)=>{
|
|
19982
|
+
if (this.cameraRecovery === recovery) this.failCameraRecovery(exc);
|
|
19983
|
+
});
|
|
19984
|
+
} else {
|
|
19985
|
+
this.userMediaLoaded = false;
|
|
19986
|
+
this.loadUserMedia({
|
|
19987
|
+
recovering: true
|
|
19988
|
+
});
|
|
19989
|
+
}
|
|
19990
|
+
return recovery;
|
|
19991
|
+
}
|
|
19992
|
+
switchFacingMode() {
|
|
19993
|
+
if (this.cameraRecovery || !util_getBrowser(this.options).isMobile()) return;
|
|
19994
|
+
if ("user" === this.facingMode) this.facingMode = "environment";
|
|
19995
|
+
else if ("environment" === this.facingMode) this.facingMode = "user";
|
|
19996
|
+
else this.options.logger.warn(`Recorder: unsupported facing mode ${string_pretty(this.facingMode)}`);
|
|
19997
|
+
this.loadGenuineUserMedia({
|
|
19998
|
+
switchingFacingMode: this.facingMode
|
|
19999
|
+
});
|
|
20000
|
+
}
|
|
20001
|
+
hide() {
|
|
20002
|
+
if (!this.isHidden()) {
|
|
20003
|
+
if (this.recorderElement) html_hideElement(this.recorderElement);
|
|
20004
|
+
this.clearUserMediaTimeout();
|
|
20005
|
+
this.clearRetryTimeout();
|
|
20006
|
+
}
|
|
20007
|
+
}
|
|
20008
|
+
isUnloaded() {
|
|
20009
|
+
return this.unloaded;
|
|
20010
|
+
}
|
|
20011
|
+
isUserMediaLoaded() {
|
|
20012
|
+
return this.userMediaLoaded;
|
|
20013
|
+
}
|
|
20014
|
+
}
|
|
20015
|
+
const media = RecorderMedia;
|
|
20016
|
+
function calculateProgress(confirmedCount, totalCount) {
|
|
20017
|
+
return `${(confirmedCount / (totalCount || 1) * 100).toFixed(2)}%`;
|
|
20018
|
+
}
|
|
20019
|
+
function getConfirmedProgressNumber({ args, kind, options }) {
|
|
20020
|
+
const label = "frame" === kind ? "frame" : "audio sample";
|
|
20021
|
+
if (!args) throw error_createError({
|
|
20022
|
+
message: `Arguments are missing for updating the ${label} progress`,
|
|
20023
|
+
options
|
|
20024
|
+
});
|
|
20025
|
+
const confirmedNumber = args[kind];
|
|
20026
|
+
if (!confirmedNumber) throw error_createError({
|
|
20027
|
+
message: `The ${label} number is missing`,
|
|
20028
|
+
options
|
|
20029
|
+
});
|
|
20030
|
+
return confirmedNumber;
|
|
20031
|
+
}
|
|
20032
|
+
var capture_Buffer = __webpack_require__("dY").Buffer;
|
|
20033
|
+
class RecorderCapture extends media {
|
|
20034
|
+
replay;
|
|
20035
|
+
loop;
|
|
20036
|
+
originalAnimationFrameObject;
|
|
20037
|
+
recordingAudioSampleRate;
|
|
20038
|
+
frameProgress;
|
|
20039
|
+
sampleProgress;
|
|
20040
|
+
canvas;
|
|
20041
|
+
ctx;
|
|
20042
|
+
stopTime;
|
|
20043
|
+
waitingTime;
|
|
20044
|
+
frame;
|
|
20045
|
+
recordingBuffer;
|
|
20046
|
+
constructor(visuals, replay, options){
|
|
20047
|
+
super(visuals, options);
|
|
20048
|
+
this.replay = replay;
|
|
20049
|
+
}
|
|
20050
|
+
onAudioSample(audioSample) {
|
|
20051
|
+
if (this.recordingTransportFailed || this.cameraRecovery) return;
|
|
20052
|
+
const sampleRate = this.userMedia?.getAudioSampleRate();
|
|
20053
|
+
if (sampleRate && sampleRate > 0) this.recordingAudioSampleRate ??= sampleRate;
|
|
20054
|
+
this.samplesCount++;
|
|
20055
|
+
this.mediaTransmissionStarted = true;
|
|
20056
|
+
const audioBuffer = audioSample.toBuffer();
|
|
20057
|
+
this.writeStream(audioBuffer);
|
|
20058
|
+
}
|
|
20059
|
+
onUserMediaReady(params) {
|
|
20060
|
+
try {
|
|
20061
|
+
this.options.logger.debug(`Recorder: onUserMediaReady(${params ? string_pretty(params) : ""})`);
|
|
20062
|
+
const switchingFacingMode = params?.switchingFacingMode;
|
|
20063
|
+
if (params?.recovering) {
|
|
20064
|
+
if (!this.userMedia?.hasLiveStream()) return void this.failCameraRecovery(new Error("The restored camera has no live tracks."));
|
|
20065
|
+
if (void 0 !== this.recordingAudioSampleRate && this.userMedia.getAudioSampleRate() !== this.recordingAudioSampleRate) return void this.failCameraRecovery(new Error("The audio sample rate changed during recovery."));
|
|
20066
|
+
}
|
|
20067
|
+
this.userMediaLoading = this.blocking = this.unloaded = this.submitting = false;
|
|
20068
|
+
this.userMediaLoaded = true;
|
|
20069
|
+
this.clearUserMediaTimeout();
|
|
20070
|
+
if (!switchingFacingMode && !params?.recovering) this.loop = this.createLoop();
|
|
20071
|
+
this.show();
|
|
20072
|
+
if (params?.recordWhenReady) this.record();
|
|
20073
|
+
this.emit("USER_MEDIA_READY", {
|
|
20074
|
+
switchingFacingMode: params?.switchingFacingMode,
|
|
20075
|
+
recovering: params?.recovering,
|
|
20076
|
+
paused: this.isPaused(),
|
|
20077
|
+
recordWhenReady: params?.recordWhenReady
|
|
20078
|
+
});
|
|
20079
|
+
if (params?.recovering) this.finishCameraRecovery(true);
|
|
20080
|
+
} catch (exc) {
|
|
20081
|
+
if (params?.recovering) this.failCameraRecovery(exc);
|
|
20082
|
+
else this.emit("ERROR", {
|
|
20083
|
+
exc
|
|
20084
|
+
});
|
|
20085
|
+
}
|
|
20086
|
+
}
|
|
20087
|
+
calculateFrameProgress() {
|
|
20088
|
+
return calculateProgress(this.confirmedFrameNumber, this.framesCount);
|
|
20089
|
+
}
|
|
20090
|
+
calculateSampleProgress() {
|
|
20091
|
+
return calculateProgress(this.confirmedSampleNumber, this.samplesCount);
|
|
20092
|
+
}
|
|
20093
|
+
updateOverallProgress() {
|
|
20094
|
+
this.frameProgress = this.calculateFrameProgress();
|
|
20095
|
+
if (isAudioEnabled(this.options)) this.sampleProgress = this.calculateSampleProgress();
|
|
20096
|
+
this.emit("PROGRESS", {
|
|
20097
|
+
frameProgress: this.frameProgress,
|
|
20098
|
+
sampleProgress: this.sampleProgress
|
|
20099
|
+
});
|
|
20100
|
+
}
|
|
20101
|
+
updateFrameProgress(args) {
|
|
20102
|
+
this.confirmedFrameNumber = getConfirmedProgressNumber({
|
|
20103
|
+
args,
|
|
20104
|
+
kind: "frame",
|
|
20105
|
+
options: this.options
|
|
20106
|
+
});
|
|
20107
|
+
this.frameProgress = this.calculateFrameProgress();
|
|
20108
|
+
this.updateOverallProgress();
|
|
20109
|
+
}
|
|
20110
|
+
updateSampleProgress(args) {
|
|
20111
|
+
this.confirmedSampleNumber = getConfirmedProgressNumber({
|
|
20112
|
+
args,
|
|
20113
|
+
kind: "sample",
|
|
20114
|
+
options: this.options
|
|
20115
|
+
});
|
|
20116
|
+
this.sampleProgress = this.calculateSampleProgress();
|
|
20117
|
+
this.updateOverallProgress();
|
|
20118
|
+
}
|
|
20119
|
+
preview(args) {
|
|
20120
|
+
if (!args) throw error_createError({
|
|
20121
|
+
message: "Preview arguments are missing.",
|
|
20122
|
+
options: this.options
|
|
20123
|
+
});
|
|
20124
|
+
const hasAudio = this.samplesCount > 0;
|
|
20125
|
+
this.mediaTransmissionStarted = false;
|
|
20126
|
+
this.recordingActive = false;
|
|
20127
|
+
this.recordingTransportFailed = false;
|
|
20128
|
+
this.confirmedFrameNumber = this.confirmedSampleNumber = this.samplesCount = this.framesCount = 0;
|
|
20129
|
+
this.sampleProgress = this.frameProgress = void 0;
|
|
20130
|
+
this.key = args.key;
|
|
20131
|
+
if (args.mp4) this.replay.setMp4Source(`${args.mp4 + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.mp4`, true);
|
|
20132
|
+
if (args.webm) this.replay.setWebMSource(`${args.webm + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.webm`, true);
|
|
20133
|
+
this.hide();
|
|
20134
|
+
const widthDimension = this.getRecorderWidth(true);
|
|
20135
|
+
const heightDimension = this.getRecorderHeight(true);
|
|
20136
|
+
const duration = args.duration ?? -1;
|
|
20137
|
+
this.emit("PREVIEW", {
|
|
20138
|
+
key: this.key,
|
|
20139
|
+
width: widthDimension?.value,
|
|
20140
|
+
height: heightDimension?.value,
|
|
20141
|
+
hasAudio,
|
|
20142
|
+
duration
|
|
20143
|
+
});
|
|
20144
|
+
if (this.stopTime) this.waitingTime = Date.now() - this.stopTime;
|
|
20145
|
+
this.recordingStats ??= {};
|
|
20146
|
+
this.recordingStats.waitingTime = this.waitingTime;
|
|
20147
|
+
}
|
|
20148
|
+
cancelAnimationFrame() {
|
|
20149
|
+
this.loop?.dispose();
|
|
20150
|
+
}
|
|
20151
|
+
getIntervalSum() {
|
|
20152
|
+
if (!this.loop) return;
|
|
20153
|
+
return this.loop.getElapsedTime();
|
|
20154
|
+
}
|
|
20155
|
+
getAvgInterval() {
|
|
20156
|
+
const intervalSum = this.getIntervalSum() ?? 0;
|
|
20157
|
+
if (!this.hasValidInterval(intervalSum)) return;
|
|
20158
|
+
return intervalSum / this.framesCount;
|
|
20159
|
+
}
|
|
20160
|
+
getAvgFps() {
|
|
20161
|
+
const intervalSum = this.getIntervalSum() ?? 0;
|
|
20162
|
+
if (!this.hasValidInterval(intervalSum)) return;
|
|
20163
|
+
return this.framesCount / intervalSum * 1000;
|
|
20164
|
+
}
|
|
20165
|
+
hasValidInterval(intervalSum) {
|
|
20166
|
+
return intervalSum > 0;
|
|
20167
|
+
}
|
|
20168
|
+
getRecordingStats() {
|
|
20169
|
+
return this.recordingStats;
|
|
20170
|
+
}
|
|
20171
|
+
getAudioSampleRate() {
|
|
20172
|
+
return this.recordingAudioSampleRate ?? this.userMedia?.getAudioSampleRate();
|
|
20173
|
+
}
|
|
20174
|
+
stop(params) {
|
|
20175
|
+
this.cancelCameraRecovery();
|
|
20176
|
+
if (!this.userMedia) throw new Error("No user media defined, unable to stop");
|
|
20177
|
+
this.options.logger.debug(`Recorder: stop(${params ? string_pretty(params) : ""})`);
|
|
20178
|
+
this.recordingActive = false;
|
|
20179
|
+
const limitReached = params?.limitReached;
|
|
20180
|
+
this.emit("STOPPING", {
|
|
20181
|
+
limitReached
|
|
20182
|
+
});
|
|
20183
|
+
this.loop?.complete?.();
|
|
20184
|
+
this.stopTimeout = window.setTimeout(()=>{
|
|
20185
|
+
this.stopTimeout = void 0;
|
|
20186
|
+
this.stopTime = Date.now();
|
|
20187
|
+
const videoType = this.replay.getVideoType();
|
|
20188
|
+
if (!videoType) throw new Error("Unable to video record when no video type is defined.");
|
|
20189
|
+
this.recordingStats = {
|
|
20190
|
+
avgFps: this.getAvgFps(),
|
|
20191
|
+
wantedFps: this.options.video.fps,
|
|
20192
|
+
avgInterval: this.getAvgInterval(),
|
|
20193
|
+
wantedInterval: 1e3 / this.options.video.fps,
|
|
20194
|
+
intervalSum: this.getIntervalSum(),
|
|
20195
|
+
framesCount: this.framesCount,
|
|
20196
|
+
videoType
|
|
20197
|
+
};
|
|
20198
|
+
if (this.userMedia) this.recordingStats.videoTrackLabel = this.userMedia.getVideoTrackLabel();
|
|
20199
|
+
if (isAudioEnabled(this.options) && this.userMedia) {
|
|
20200
|
+
this.recordingStats.samplesCount = this.samplesCount;
|
|
20201
|
+
this.recordingStats.sampleRate = this.recordingAudioSampleRate ?? this.userMedia.getAudioSampleRate();
|
|
20202
|
+
}
|
|
20203
|
+
this.writeCommand("stop", this.recordingStats, ()=>{
|
|
20204
|
+
this.emit("STOPPED", {
|
|
20205
|
+
recordingStats: this.recordingStats
|
|
20206
|
+
});
|
|
20207
|
+
});
|
|
20208
|
+
this.reset();
|
|
20209
|
+
}, 60);
|
|
20210
|
+
}
|
|
20211
|
+
back(cb) {
|
|
20212
|
+
this.emit("GOING_BACK");
|
|
20213
|
+
this.key = void 0;
|
|
20214
|
+
this.recordingDimensions = void 0;
|
|
20215
|
+
this.unloaded = false;
|
|
20216
|
+
this.show();
|
|
20217
|
+
this.writeCommand("back", void 0, cb);
|
|
20218
|
+
}
|
|
20219
|
+
reInitializeAudio() {
|
|
20220
|
+
this.options.logger.debug("Recorder: reInitializeAudio()");
|
|
20221
|
+
this.clearUserMediaTimeout();
|
|
20222
|
+
this.userMediaLoading = false;
|
|
20223
|
+
this.userMedia?.stop();
|
|
20224
|
+
this.recordingDimensions = void 0;
|
|
20225
|
+
this.userMediaLoaded = this.key = this.canvas = this.ctx = void 0;
|
|
20226
|
+
this.loadUserMedia();
|
|
20227
|
+
}
|
|
20228
|
+
reset() {
|
|
20229
|
+
this.cancelCameraRecovery();
|
|
20230
|
+
if (!this.unloaded) {
|
|
20231
|
+
this.options.logger.debug("Recorder: reset()");
|
|
20232
|
+
this.emit("RESETTING");
|
|
20233
|
+
this.cancelAnimationFrame();
|
|
20234
|
+
this.userMedia?.stop();
|
|
20235
|
+
this.replay.reset();
|
|
20236
|
+
this.userMediaLoaded = this.key = this.canvas = this.ctx = this.recordingBuffer = void 0;
|
|
20237
|
+
}
|
|
20238
|
+
}
|
|
20239
|
+
validate() {
|
|
20240
|
+
return Boolean(this.key) && void 0 === this.canvas;
|
|
20241
|
+
}
|
|
20242
|
+
pause(params) {
|
|
20243
|
+
this.cancelCameraRecovery();
|
|
20244
|
+
this.resumeAfterDrain = false;
|
|
20245
|
+
const loop = this.loop;
|
|
20246
|
+
if (!loop) throw new Error("No recording loop defined");
|
|
20247
|
+
if (params) this.options.logger.debug(`pause() at frame ${this.framesCount} with ${string_pretty(params)}`);
|
|
20248
|
+
else this.options.logger.debug(`pause() at frame ${this.framesCount}`);
|
|
20249
|
+
this.userMedia?.pause();
|
|
20250
|
+
loop.stop();
|
|
20251
|
+
this.emit("PAUSED");
|
|
20252
|
+
this.sendPings();
|
|
20253
|
+
}
|
|
20254
|
+
resume() {
|
|
20255
|
+
if (this.cameraRecovery || !this.userMedia?.hasLiveStream()) {
|
|
20256
|
+
this.recoverCamera();
|
|
20257
|
+
this.resumeAfterCameraRecovery = Boolean(this.cameraRecovery);
|
|
20258
|
+
return;
|
|
20259
|
+
}
|
|
20260
|
+
const loop = this.loop;
|
|
20261
|
+
if (!loop) throw new Error("No recording loop defined");
|
|
20262
|
+
this.options.logger.debug(`Recorder: resume() with frame ${this.framesCount}`);
|
|
20263
|
+
this.stopPings();
|
|
20264
|
+
this.emit("RESUMING");
|
|
20265
|
+
this.userMedia.resume();
|
|
20266
|
+
if (this.streamBackpressured) this.resumeAfterDrain = true;
|
|
20267
|
+
else loop.start();
|
|
20268
|
+
}
|
|
20269
|
+
onFlushed(opts) {
|
|
20270
|
+
const frameNumber = opts.frameNumber;
|
|
20271
|
+
if (1 === frameNumber) this.emit("FIRST_FRAME_SENT");
|
|
20272
|
+
}
|
|
20273
|
+
draw(_deltaTime, elapsedTime) {
|
|
20274
|
+
if (this.recordingTransportFailed) return;
|
|
20275
|
+
if (!this.userMedia) throw new Error("No user media defined, unable to draw on canvas");
|
|
20276
|
+
try {
|
|
20277
|
+
if (!this.isPaused() && this.stream && this.ctx) {
|
|
20278
|
+
if (0 === this.framesCount) this.emit("SENDING_FIRST_FRAME");
|
|
20279
|
+
this.framesCount++;
|
|
20280
|
+
const imageSource = this.userMedia.getRawVisuals();
|
|
20281
|
+
if (this.canvas && imageSource) this.ctx.drawImage(imageSource, 0, 0, this.canvas.width, this.canvas.height);
|
|
20282
|
+
else throw new Error("Unable to draw an image without a defined canvas");
|
|
20283
|
+
this.recordingBuffer = this.frame?.toBuffer();
|
|
20284
|
+
const recordingBufferLength = this.recordingBuffer?.length;
|
|
20285
|
+
if (recordingBufferLength) {
|
|
20286
|
+
if (this.recordingBuffer) {
|
|
20287
|
+
const frameControlBuffer = capture_Buffer.from(JSON.stringify({
|
|
20288
|
+
frameNumber: this.framesCount
|
|
20289
|
+
}));
|
|
20290
|
+
const frameBuffer = capture_Buffer.concat([
|
|
20291
|
+
this.recordingBuffer,
|
|
20292
|
+
frameControlBuffer
|
|
20293
|
+
]);
|
|
20294
|
+
this.mediaTransmissionStarted = true;
|
|
20295
|
+
this.writeStream(frameBuffer, {
|
|
20296
|
+
frameNumber: this.framesCount,
|
|
20297
|
+
onFlushedCallback: this.onFlushed.bind(this)
|
|
20298
|
+
});
|
|
20299
|
+
this.visuals.checkTimer(elapsedTime);
|
|
20300
|
+
}
|
|
20301
|
+
} else throw error_createError({
|
|
20302
|
+
message: "Failed to extract webcam data.",
|
|
20303
|
+
options: this.options
|
|
20304
|
+
});
|
|
20305
|
+
}
|
|
20306
|
+
} catch (exc) {
|
|
20307
|
+
this.emit("ERROR", {
|
|
20308
|
+
exc
|
|
20309
|
+
});
|
|
20310
|
+
}
|
|
20311
|
+
}
|
|
20312
|
+
createLoop() {
|
|
20313
|
+
const newLoop = animitter_default()({
|
|
20314
|
+
fps: this.options.video.fps
|
|
20315
|
+
}, this.draw.bind(this));
|
|
20316
|
+
this.originalAnimationFrameObject = newLoop.getRequestAnimationFrameObject();
|
|
20317
|
+
return newLoop;
|
|
20318
|
+
}
|
|
20319
|
+
record() {
|
|
20320
|
+
if (this.unloaded || this.cameraRecovery) return;
|
|
20321
|
+
if (!this.connected) {
|
|
20322
|
+
this.options.logger.debug("Recorder: reconnecting before recording …");
|
|
20323
|
+
this.initSocket(()=>{
|
|
20324
|
+
this.once("SERVER_READY", ()=>{
|
|
20325
|
+
if (this.unloaded) return;
|
|
20326
|
+
if (this.userMediaLoaded || this.options.loadUserMediaOnRecord) this.record();
|
|
20327
|
+
else this.once("USER_MEDIA_READY", ()=>{
|
|
20328
|
+
if (!this.unloaded) this.record();
|
|
20329
|
+
});
|
|
20330
|
+
});
|
|
20331
|
+
});
|
|
20332
|
+
return;
|
|
20333
|
+
}
|
|
20334
|
+
if (!this.userMediaLoaded) {
|
|
20335
|
+
if (this.options.loadUserMediaOnRecord) this.loadUserMedia({
|
|
20336
|
+
recordWhenReady: true
|
|
20337
|
+
});
|
|
20338
|
+
else {
|
|
20339
|
+
const err = error_createError({
|
|
20340
|
+
message: "Load and enable your camera first",
|
|
20341
|
+
options: this.options
|
|
20342
|
+
});
|
|
20343
|
+
this.emit("ERROR", {
|
|
20344
|
+
err
|
|
20345
|
+
});
|
|
20346
|
+
}
|
|
20347
|
+
return;
|
|
20348
|
+
}
|
|
20349
|
+
const loop = this.loop;
|
|
20350
|
+
if (!loop) throw new Error("No recording loop defined");
|
|
20351
|
+
this.recordingDimensions = void 0;
|
|
20352
|
+
try {
|
|
20353
|
+
if (!this.userMedia) throw new Error("No user media defined, unable to create canvas");
|
|
20354
|
+
this.canvas = this.userMedia.createCanvas();
|
|
20355
|
+
} catch (exc) {
|
|
20356
|
+
const err = error_createError({
|
|
20357
|
+
exc,
|
|
20358
|
+
options: this.options
|
|
20359
|
+
});
|
|
20360
|
+
this.emit("ERROR", {
|
|
20361
|
+
err
|
|
20362
|
+
});
|
|
20363
|
+
return;
|
|
20364
|
+
}
|
|
20365
|
+
this.ctx = this.canvas.getContext("2d");
|
|
20366
|
+
if (!this.canvas.width) {
|
|
20367
|
+
const err = error_createError({
|
|
20368
|
+
message: "Canvas has an invalid width.",
|
|
20369
|
+
options: this.options
|
|
20370
|
+
});
|
|
20371
|
+
this.emit("ERROR", {
|
|
20372
|
+
err
|
|
20373
|
+
});
|
|
20374
|
+
return;
|
|
20375
|
+
}
|
|
20376
|
+
if (!this.canvas.height) {
|
|
20377
|
+
const err = error_createError({
|
|
20378
|
+
message: "Canvas has an invalid height.",
|
|
20379
|
+
options: this.options
|
|
20380
|
+
});
|
|
20381
|
+
this.emit("ERROR", {
|
|
20382
|
+
err
|
|
20383
|
+
});
|
|
20384
|
+
return;
|
|
20385
|
+
}
|
|
20386
|
+
this.recordingDimensions = {
|
|
20387
|
+
width: this.canvas.width,
|
|
20388
|
+
height: this.canvas.height
|
|
20389
|
+
};
|
|
20390
|
+
this.frame = new canvas_to_buffer_modern_r(this.canvas, this.options.image.types, this.options.image.quality);
|
|
20391
|
+
this.options.logger.debug("Recorder: record()");
|
|
20392
|
+
this.recordingActive = true;
|
|
20393
|
+
this.recordingTransportFailed = false;
|
|
20394
|
+
this.mediaTransmissionStarted = false;
|
|
20395
|
+
this.recordingAudioSampleRate = void 0;
|
|
20396
|
+
this.userMedia.record();
|
|
20397
|
+
this.emit("RECORDING", {
|
|
20398
|
+
framesCount: this.framesCount
|
|
20399
|
+
});
|
|
20400
|
+
loop.on("update", (_deltaTime, elapsedTime)=>{
|
|
20401
|
+
let avgFPS;
|
|
20402
|
+
avgFPS = 0 !== elapsedTime ? Math.round(this.framesCount / elapsedTime * 1000) : void 0;
|
|
20403
|
+
this.options.logger.debug(`Recorder updates avgFps = ${avgFPS} at frame ${this.framesCount}`);
|
|
20404
|
+
});
|
|
20405
|
+
loop.start();
|
|
20406
|
+
}
|
|
20407
|
+
setAnimationFrameObject(newObj) {
|
|
20408
|
+
if (this.loop) {
|
|
20409
|
+
const isRecording = this.isRecording();
|
|
20410
|
+
this.loop.stop();
|
|
20411
|
+
this.loop.setRequestAnimationFrameObject(newObj);
|
|
20412
|
+
if (isRecording) this.loop.start();
|
|
20413
|
+
}
|
|
20414
|
+
}
|
|
20415
|
+
restoreAnimationFrameObject() {
|
|
20416
|
+
this.options.logger.debug("Recorder: restoreAnimationFrameObject()");
|
|
20417
|
+
if (this.originalAnimationFrameObject) this.setAnimationFrameObject(this.originalAnimationFrameObject);
|
|
20418
|
+
}
|
|
20419
|
+
loopWithTimeouts() {
|
|
20420
|
+
this.options.logger.debug("Recorder: loopWithTimeouts()");
|
|
20421
|
+
const wantedInterval = 1e3 / this.options.video.fps;
|
|
20422
|
+
let processingTime = 0;
|
|
20423
|
+
const raf = (fn)=>window.setTimeout(()=>{
|
|
20424
|
+
const start = Date.now();
|
|
20425
|
+
fn();
|
|
20426
|
+
processingTime = Date.now() - start;
|
|
20427
|
+
}, wantedInterval - processingTime);
|
|
20428
|
+
const cancel = (id)=>{
|
|
20429
|
+
window.clearTimeout(id);
|
|
20430
|
+
};
|
|
20431
|
+
this.setAnimationFrameObject({
|
|
20432
|
+
requestAnimationFrame: raf,
|
|
20433
|
+
cancelAnimationFrame: cancel
|
|
20434
|
+
});
|
|
20435
|
+
}
|
|
20436
|
+
isPaused() {
|
|
20437
|
+
return this.userMedia?.isPaused() && !this.loop?.isRunning();
|
|
20438
|
+
}
|
|
20439
|
+
isUserMediaPaused() {
|
|
20440
|
+
return this.userMedia?.isPaused() ?? false;
|
|
20441
|
+
}
|
|
20442
|
+
isCaptureLoopRunning() {
|
|
20443
|
+
return this.loop?.isRunning() ?? false;
|
|
20444
|
+
}
|
|
20445
|
+
stopCaptureLoop() {
|
|
20446
|
+
this.loop?.stop();
|
|
20447
|
+
}
|
|
20448
|
+
startCaptureLoop() {
|
|
20449
|
+
this.loop?.start();
|
|
20450
|
+
}
|
|
20451
|
+
isRecording() {
|
|
20452
|
+
return Boolean(this.loop?.isRunning() && !this.isPaused() && !this.isNotifying() && this.stream && !this.stream.destroyed);
|
|
20453
|
+
}
|
|
20454
|
+
getRecordingDimensions() {
|
|
20455
|
+
return this.recordingDimensions && {
|
|
20456
|
+
...this.recordingDimensions
|
|
20457
|
+
};
|
|
20458
|
+
}
|
|
20459
|
+
}
|
|
20460
|
+
const capture = RecorderCapture;
|
|
20461
|
+
function figureMinHeight(height, options) {
|
|
20462
|
+
let minHeight;
|
|
20463
|
+
if (options.video.height) {
|
|
20464
|
+
minHeight = height ? Math.min(options.video.height, height) : options.video.height;
|
|
20465
|
+
if (minHeight < 1) throw error_createError({
|
|
20466
|
+
message: `Got a min height less than 1 (${minHeight})!`,
|
|
20467
|
+
options
|
|
20468
|
+
});
|
|
20469
|
+
} else minHeight = height;
|
|
20470
|
+
return minHeight;
|
|
20471
|
+
}
|
|
20472
|
+
const dimensions_figureMinHeight = figureMinHeight;
|
|
20473
|
+
function getRatio(options, videoHeight, videoWidth) {
|
|
20474
|
+
let ratio = 1;
|
|
20475
|
+
const hasVideoDimensions = videoHeight && videoWidth;
|
|
20476
|
+
const desiredHeight = options.video.height;
|
|
20477
|
+
const desiredWidth = options.video.width;
|
|
20478
|
+
const hasDesiredDimensions = desiredHeight && desiredWidth;
|
|
20479
|
+
if (hasDesiredDimensions) ratio = hasVideoDimensions ? videoHeight < desiredHeight || videoWidth < desiredWidth ? videoHeight / videoWidth : desiredHeight / desiredWidth : desiredHeight / desiredWidth;
|
|
20480
|
+
else if (hasVideoDimensions) ratio = videoHeight / videoWidth;
|
|
20481
|
+
return ratio;
|
|
20482
|
+
}
|
|
20483
|
+
const dimensions_getRatio = getRatio;
|
|
20484
|
+
function calculateHeight(responsive, videoWidth, options, ratio, element) {
|
|
20485
|
+
const dimension = {
|
|
20486
|
+
unit: "px"
|
|
20487
|
+
};
|
|
20488
|
+
let width = videoWidth;
|
|
20489
|
+
if (responsive && element) {
|
|
20490
|
+
const limitedDimension = dimensions_limitWidth(element, options, width);
|
|
20491
|
+
width = limitedDimension.value;
|
|
20492
|
+
}
|
|
20493
|
+
const chosenRatio = ratio ?? dimensions_getRatio(options, void 0, videoWidth);
|
|
20494
|
+
const height = width ? Math.round(width * chosenRatio) : void 0;
|
|
20495
|
+
if (height && Number.isInteger(height) && height < 1) throw error_createError({
|
|
20496
|
+
message: "Just calculated a height less than 1 which is wrong.",
|
|
20497
|
+
options
|
|
20498
|
+
});
|
|
20499
|
+
const minHeight = dimensions_figureMinHeight(height, options);
|
|
20500
|
+
dimension.value = minHeight;
|
|
20501
|
+
return dimension;
|
|
20502
|
+
}
|
|
20503
|
+
const dimensions_calculateHeight = calculateHeight;
|
|
20504
|
+
function calculateWidth(responsive, options, videoHeight, ratio) {
|
|
20505
|
+
const dimension = {
|
|
20506
|
+
unit: "px"
|
|
20507
|
+
};
|
|
20508
|
+
let height = dimensions_figureMinHeight(videoHeight, options);
|
|
20509
|
+
if (responsive) {
|
|
20510
|
+
const limitedDimension = dimensions_limitHeight(height, options, "calculateWidth");
|
|
20511
|
+
height = limitedDimension.value;
|
|
20512
|
+
}
|
|
20513
|
+
if (!height || height < 1) throw error_createError({
|
|
20514
|
+
message: `Height ${height} cannot be smaller than 1 when calculating width.`,
|
|
20515
|
+
options
|
|
20516
|
+
});
|
|
20517
|
+
const chosenRatio = ratio ?? dimensions_getRatio(options, videoHeight);
|
|
20518
|
+
const calculatedWidth = Math.round(height / chosenRatio);
|
|
20519
|
+
if (calculatedWidth < 1) throw error_createError({
|
|
20520
|
+
message: "Calculated width cannot be smaller than 1!",
|
|
20521
|
+
options
|
|
20522
|
+
});
|
|
20523
|
+
dimension.value = calculatedWidth;
|
|
20524
|
+
return dimension;
|
|
20525
|
+
}
|
|
20526
|
+
const dimensions_calculateWidth = calculateWidth;
|
|
20527
|
+
class RecorderDimensions {
|
|
20528
|
+
context;
|
|
20529
|
+
constructor(context){
|
|
20530
|
+
this.context = context;
|
|
20531
|
+
}
|
|
20532
|
+
correctDimensions() {
|
|
20533
|
+
const recorderElement = this.context.getRecorderElement();
|
|
20534
|
+
if (!recorderElement) return;
|
|
20535
|
+
const widthDimension = dimensions_useFullWidth(this.context.options.video.mobileBreakPoint);
|
|
20536
|
+
if (this.context.options.video.stretch || widthDimension) {
|
|
20537
|
+
recorderElement.style.width = "100%";
|
|
20538
|
+
recorderElement.style.removeProperty("height");
|
|
20539
|
+
} else {
|
|
20540
|
+
if (this.context.options.video.width) {
|
|
20541
|
+
const recorderWidth = this.getRecorderWidth(true);
|
|
20542
|
+
if (recorderWidth?.value) recorderElement.width = recorderWidth.value;
|
|
20543
|
+
else recorderElement.style.removeProperty("width");
|
|
20544
|
+
}
|
|
20545
|
+
if (this.context.options.video.height) {
|
|
20546
|
+
const recorderHeight = this.getRecorderHeight(true);
|
|
20547
|
+
if (recorderHeight?.value) recorderElement.height = recorderHeight.value;
|
|
20548
|
+
else recorderElement.style.removeProperty("height");
|
|
20549
|
+
}
|
|
20550
|
+
}
|
|
20551
|
+
}
|
|
20552
|
+
getRecorderWidth(responsive) {
|
|
20553
|
+
const userMedia = this.context.getUserMedia();
|
|
20554
|
+
if (userMedia?.hasVideoWidth()) return userMedia.getRawWidth(responsive);
|
|
20555
|
+
else if (responsive && this.context.options.video.width) return this.context.visuals.limitWidth(this.context.options.video.width);
|
|
20556
|
+
const dimension = {
|
|
20557
|
+
unit: "px",
|
|
20558
|
+
value: this.context.options.video.width
|
|
20559
|
+
};
|
|
20560
|
+
return dimension;
|
|
20561
|
+
}
|
|
20562
|
+
getRecorderHeight(responsive, useBoundingClientRect) {
|
|
20563
|
+
const recorderElement = this.context.getRecorderElement();
|
|
20564
|
+
const userMedia = this.context.getUserMedia();
|
|
20565
|
+
let recorderHeight;
|
|
20566
|
+
if (recorderElement && useBoundingClientRect) {
|
|
20567
|
+
const height = recorderElement.getBoundingClientRect().height;
|
|
20568
|
+
recorderHeight = {
|
|
20569
|
+
unit: "px",
|
|
20570
|
+
value: height
|
|
20571
|
+
};
|
|
20572
|
+
} else if (userMedia) {
|
|
20573
|
+
let height = userMedia.getRawHeight(responsive);
|
|
20574
|
+
if (void 0 !== height && this.context.options.video.width) {
|
|
20575
|
+
const ratio = this.getRatio();
|
|
20576
|
+
if (void 0 !== ratio) {
|
|
20577
|
+
const idealHeight = this.context.options.video.width * ratio;
|
|
20578
|
+
height = Math.min(idealHeight, height);
|
|
20579
|
+
}
|
|
20580
|
+
}
|
|
20581
|
+
recorderHeight = {
|
|
20582
|
+
unit: "px",
|
|
20583
|
+
value: height
|
|
20584
|
+
};
|
|
20585
|
+
} else if (responsive && this.context.options.video.height) recorderHeight = this.calculateHeight(responsive);
|
|
20586
|
+
else if (this.context.options.video.height) {
|
|
20587
|
+
const height = this.context.options.video.height;
|
|
20588
|
+
recorderHeight = {
|
|
20589
|
+
unit: "px",
|
|
20590
|
+
value: height
|
|
20591
|
+
};
|
|
20592
|
+
}
|
|
20593
|
+
return recorderHeight;
|
|
20594
|
+
}
|
|
20595
|
+
getRatio() {
|
|
20596
|
+
const userMedia = this.context.getUserMedia();
|
|
20597
|
+
let ratio;
|
|
20598
|
+
if (userMedia) {
|
|
20599
|
+
const userMediaVideoWidth = userMedia.getVideoWidth();
|
|
20600
|
+
const userMediaVideoHeight = userMedia.getVideoHeight();
|
|
20601
|
+
if (!userMediaVideoWidth || userMediaVideoWidth < 1) ratio = this.context.visuals.getRatio();
|
|
20602
|
+
else if (userMediaVideoHeight) ratio = userMediaVideoHeight / userMediaVideoWidth;
|
|
20603
|
+
} else ratio = dimensions_getRatio(this.context.options);
|
|
20604
|
+
return ratio;
|
|
20605
|
+
}
|
|
20606
|
+
calculateWidth(responsive) {
|
|
20607
|
+
const userMedia = this.context.getUserMedia();
|
|
20608
|
+
const recorderElement = this.context.getRecorderElement();
|
|
20609
|
+
let videoHeight;
|
|
20610
|
+
if (userMedia) videoHeight = userMedia.getVideoHeight();
|
|
20611
|
+
else if (recorderElement) videoHeight = recorderElement.videoHeight || recorderElement.height;
|
|
20612
|
+
return dimensions_calculateWidth(responsive, this.context.options, videoHeight, this.getRatio());
|
|
20613
|
+
}
|
|
20614
|
+
calculateHeight(responsive) {
|
|
20615
|
+
const userMedia = this.context.getUserMedia();
|
|
20616
|
+
const recorderElement = this.context.getRecorderElement();
|
|
20617
|
+
let videoDimension;
|
|
20618
|
+
if (userMedia) {
|
|
20619
|
+
const videoHeight = userMedia.getVideoHeight();
|
|
20620
|
+
videoDimension = {
|
|
20621
|
+
value: videoHeight,
|
|
20622
|
+
unit: "px"
|
|
20623
|
+
};
|
|
20624
|
+
} else if (recorderElement) {
|
|
20625
|
+
const videoHeight = recorderElement.videoHeight || recorderElement.height;
|
|
20626
|
+
videoDimension = {
|
|
20627
|
+
value: videoHeight,
|
|
20628
|
+
unit: "px"
|
|
20629
|
+
};
|
|
20630
|
+
} else videoDimension = dimensions_calculateHeight(responsive, void 0, this.context.options, this.getRatio(), recorderElement);
|
|
20631
|
+
return videoDimension;
|
|
20632
|
+
}
|
|
20633
|
+
}
|
|
20634
|
+
const recorder_dimensions = RecorderDimensions;
|
|
20635
|
+
function getFirstVideoTrack(localMediaStream) {
|
|
20636
|
+
const videoTracks = localMediaStream.getVideoTracks();
|
|
20637
|
+
let videoTrack;
|
|
20638
|
+
if (videoTracks[0]) videoTrack = videoTracks[0];
|
|
20639
|
+
return videoTrack;
|
|
20640
|
+
}
|
|
20641
|
+
const media_getFirstVideoTrack = getFirstVideoTrack;
|
|
20642
|
+
const MEDIA_EVENTS = [
|
|
20643
|
+
"loadstart",
|
|
20644
|
+
"suspend",
|
|
20645
|
+
"progress",
|
|
20646
|
+
"abort",
|
|
20647
|
+
"emptied",
|
|
20648
|
+
"stalled",
|
|
20649
|
+
"pause",
|
|
20650
|
+
"loadeddata",
|
|
20651
|
+
"waiting",
|
|
20652
|
+
"playing",
|
|
20653
|
+
"canplay",
|
|
20654
|
+
"canplaythrough",
|
|
20655
|
+
"seeking",
|
|
20656
|
+
"seeked",
|
|
20657
|
+
"ended",
|
|
20658
|
+
"ratechange",
|
|
20659
|
+
"durationchange",
|
|
20660
|
+
"volumechange"
|
|
20661
|
+
];
|
|
20662
|
+
const mediaEvents = MEDIA_EVENTS;
|
|
20663
|
+
function _define_property(obj, key, value) {
|
|
20664
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
20665
|
+
value: value,
|
|
20666
|
+
enumerable: true,
|
|
20667
|
+
configurable: true,
|
|
20668
|
+
writable: true
|
|
20669
|
+
});
|
|
20670
|
+
else obj[key] = value;
|
|
20671
|
+
return obj;
|
|
20672
|
+
}
|
|
20673
|
+
class AudioSample {
|
|
20674
|
+
toBuffer() {
|
|
20675
|
+
const l = this.float32Array.length;
|
|
20676
|
+
const arr = new Int16Array(l);
|
|
20677
|
+
this.float32Array.forEach((sample, i)=>{
|
|
20678
|
+
const clampedSample = Number.isFinite(sample) ? Math.max(-1, sample) : sample;
|
|
20679
|
+
arr[i] = 0x7fff * Math.min(1, clampedSample);
|
|
20680
|
+
});
|
|
20681
|
+
return typedarray_to_buffer(arr);
|
|
20682
|
+
}
|
|
20683
|
+
constructor(float32Array){
|
|
20684
|
+
_define_property(this, "float32Array", void 0);
|
|
20685
|
+
this.float32Array = float32Array;
|
|
20686
|
+
}
|
|
20687
|
+
}
|
|
20688
|
+
const esm_src = AudioSample;
|
|
20689
|
+
const esm = esm_src;
|
|
20690
|
+
var is_power_of_two = __webpack_require__("dZ");
|
|
20691
|
+
var is_power_of_two_default = /*#__PURE__*/ __webpack_require__.n(is_power_of_two);
|
|
20692
|
+
const CHANNELS = 1;
|
|
20693
|
+
function getAudioContextClass() {
|
|
20694
|
+
return window.AudioContext;
|
|
20695
|
+
}
|
|
20696
|
+
class AudioRecorder {
|
|
20697
|
+
scriptProcessor;
|
|
20698
|
+
audioInput;
|
|
20699
|
+
volume;
|
|
20700
|
+
vcAudioContext;
|
|
20701
|
+
userMedia;
|
|
20702
|
+
options;
|
|
20703
|
+
constructor(userMedia, options){
|
|
20704
|
+
this.options = options;
|
|
20705
|
+
this.userMedia = userMedia;
|
|
20706
|
+
}
|
|
20707
|
+
hasAudioContext() {
|
|
20708
|
+
return Boolean(getAudioContextClass()) && Boolean(this.getAudioContext());
|
|
20709
|
+
}
|
|
20710
|
+
getAudioContext() {
|
|
20711
|
+
if (!this.vcAudioContext) {
|
|
20712
|
+
const AudioContext = getAudioContextClass();
|
|
20713
|
+
this.vcAudioContext = new AudioContext();
|
|
20714
|
+
}
|
|
20715
|
+
return this.vcAudioContext;
|
|
20716
|
+
}
|
|
20717
|
+
onAudioProcess(e, cb) {
|
|
20718
|
+
if (!this.userMedia.isRecording() || this.userMedia.isPaused()) return;
|
|
20719
|
+
const float32Array = e.inputBuffer.getChannelData(0);
|
|
20720
|
+
cb(new esm(float32Array));
|
|
20721
|
+
}
|
|
20722
|
+
disconnect() {
|
|
20723
|
+
if (this.scriptProcessor) {
|
|
20724
|
+
this.scriptProcessor.onaudioprocess = null;
|
|
20725
|
+
this.scriptProcessor.disconnect();
|
|
20726
|
+
}
|
|
20727
|
+
this.audioInput?.disconnect();
|
|
20728
|
+
this.volume?.disconnect();
|
|
20729
|
+
}
|
|
20730
|
+
init(localMediaStream) {
|
|
20731
|
+
this.options.logger.debug("AudioRecorder: init()");
|
|
20732
|
+
this.disconnect();
|
|
20733
|
+
const volume = this.getAudioContext().createGain();
|
|
20734
|
+
this.volume = volume;
|
|
20735
|
+
try {
|
|
20736
|
+
this.audioInput = this.getAudioContext().createMediaStreamSource(localMediaStream);
|
|
20737
|
+
} catch (exc) {
|
|
20738
|
+
throw error_createError({
|
|
20739
|
+
message: "Webcam has no audio",
|
|
20740
|
+
exc,
|
|
20741
|
+
options: this.options
|
|
20742
|
+
});
|
|
20743
|
+
}
|
|
20744
|
+
let { bufferSize } = this.options.audio;
|
|
20745
|
+
if ("auto" === bufferSize) bufferSize = util_getBrowser(this.options).isFirefox() ? 512 : 2048;
|
|
20746
|
+
if (void 0 === bufferSize || !is_power_of_two_default()(bufferSize)) throw error_createError({
|
|
20747
|
+
message: `Audio buffer size must be a power of two. The current buffer size is ${bufferSize}.`,
|
|
20748
|
+
options: this.options
|
|
20749
|
+
});
|
|
20750
|
+
if (!Number.isFinite(this.options.audio.volume) || this.options.audio.volume < 0 || this.options.audio.volume > 1) throw error_createError({
|
|
20751
|
+
message: `Audio volume must be between zero and one. The current volume is ${this.options.audio.volume}.`,
|
|
20752
|
+
options: this.options
|
|
20753
|
+
});
|
|
20754
|
+
volume.gain.value = this.options.audio.volume;
|
|
20755
|
+
this.scriptProcessor = this.getAudioContext().createScriptProcessor(bufferSize, CHANNELS, CHANNELS);
|
|
20756
|
+
this.audioInput.connect(volume);
|
|
20757
|
+
volume.connect(this.scriptProcessor);
|
|
20758
|
+
this.scriptProcessor.connect(this.getAudioContext().destination);
|
|
20759
|
+
}
|
|
20760
|
+
record(cb) {
|
|
20761
|
+
this.options.logger.debug("AudioRecorder: record()");
|
|
20762
|
+
if (this.scriptProcessor) this.scriptProcessor.onaudioprocess = (e)=>{
|
|
20763
|
+
this.onAudioProcess(e, cb);
|
|
20764
|
+
};
|
|
20765
|
+
}
|
|
20766
|
+
async resume() {
|
|
20767
|
+
const context = this.getAudioContext();
|
|
20768
|
+
if ("running" !== context.state) await context.resume();
|
|
20769
|
+
}
|
|
20770
|
+
stop() {
|
|
20771
|
+
this.options.logger.debug("AudioRecorder: stop()");
|
|
20772
|
+
this.disconnect();
|
|
20773
|
+
if (this.hasAudioContext()) this.getAudioContext().close().then(()=>{
|
|
20774
|
+
this.options.logger.debug("AudioRecorder: audio context is closed");
|
|
20775
|
+
this.vcAudioContext = void 0;
|
|
20776
|
+
}).catch((err)=>{
|
|
20777
|
+
if (err instanceof Error) return void this.options.logger.error(error_createError({
|
|
20778
|
+
err,
|
|
20779
|
+
options: this.options
|
|
20780
|
+
}));
|
|
20781
|
+
this.options.logger.error(err);
|
|
20782
|
+
});
|
|
20783
|
+
}
|
|
20784
|
+
getSampleRate() {
|
|
20785
|
+
if (this.hasAudioContext()) return this.getAudioContext().sampleRate;
|
|
20786
|
+
return -1;
|
|
20787
|
+
}
|
|
20788
|
+
}
|
|
20789
|
+
const media_AudioRecorder = AudioRecorder;
|
|
20790
|
+
const VIRTUAL_KEYWORDS = [
|
|
20791
|
+
"obs",
|
|
20792
|
+
"virtual",
|
|
20793
|
+
"manycam",
|
|
20794
|
+
"vcam",
|
|
20795
|
+
"splitcam",
|
|
20796
|
+
"droidcam"
|
|
20797
|
+
];
|
|
20798
|
+
function isVirtualCamera(videoTrack) {
|
|
20799
|
+
const capabilities = videoTrack.getCapabilities();
|
|
20800
|
+
if (!capabilities.frameRate) return true;
|
|
20801
|
+
const isVirtual = VIRTUAL_KEYWORDS.some((keyword)=>videoTrack.label.toLowerCase().includes(keyword));
|
|
20802
|
+
return isVirtual;
|
|
20803
|
+
}
|
|
20804
|
+
const util_isVirtualCamera = isVirtualCamera;
|
|
20805
|
+
const EVENT_ASCII = "|—O—|";
|
|
20806
|
+
class UserMedia extends util_Despot {
|
|
20807
|
+
recorder;
|
|
20808
|
+
rawVisualUserMedia;
|
|
20809
|
+
paused = false;
|
|
20810
|
+
recording = false;
|
|
20811
|
+
audioRecorder;
|
|
20812
|
+
currentVisualStream;
|
|
20813
|
+
onPlayReached = false;
|
|
20814
|
+
onLoadedMetaDataReached = false;
|
|
20815
|
+
playingPromiseReached = false;
|
|
20816
|
+
videoTrackLabel;
|
|
20817
|
+
clearInitializationListeners;
|
|
20818
|
+
initializationId = 0;
|
|
20819
|
+
outputEvent = (e)=>{
|
|
20820
|
+
this.logEvent(e.type, {
|
|
20821
|
+
readyState: this.rawVisualUserMedia?.readyState
|
|
20822
|
+
});
|
|
20823
|
+
this.rawVisualUserMedia?.removeEventListener(e.type, this.outputEvent);
|
|
20824
|
+
};
|
|
20825
|
+
handleMediaError = (event)=>{
|
|
20826
|
+
this.options.logger.warn(`Caught video element error event: ${string_pretty(event)}`);
|
|
20827
|
+
};
|
|
20828
|
+
constructor(recorder, options){
|
|
20829
|
+
super("UserMedia", options);
|
|
20830
|
+
this.recorder = recorder;
|
|
20831
|
+
this.rawVisualUserMedia = recorder.getRawVisualUserMedia();
|
|
20832
|
+
mediaEvents.forEach((eventName)=>{
|
|
20833
|
+
this.rawVisualUserMedia?.addEventListener(eventName, this.outputEvent, false);
|
|
20834
|
+
});
|
|
20835
|
+
}
|
|
20836
|
+
attachMediaStream(stream) {
|
|
20837
|
+
if (this.rawVisualUserMedia) {
|
|
20838
|
+
this.rawVisualUserMedia.srcObject = stream;
|
|
20839
|
+
const previousStream = this.currentVisualStream;
|
|
20840
|
+
this.currentVisualStream = stream;
|
|
20841
|
+
if (previousStream && previousStream !== stream) previousStream.getTracks().forEach((track)=>{
|
|
20842
|
+
track.stop();
|
|
20843
|
+
});
|
|
20844
|
+
} else throw error_createError({
|
|
20845
|
+
message: "Error attaching stream to element.",
|
|
20846
|
+
explanation: "Contact the developer about this",
|
|
20847
|
+
options: this.options
|
|
20848
|
+
});
|
|
20849
|
+
}
|
|
20850
|
+
setVisualStream(localMediaStream) {
|
|
20851
|
+
if (localMediaStream) this.attachMediaStream(localMediaStream);
|
|
20852
|
+
else {
|
|
20853
|
+
if (this.rawVisualUserMedia) {
|
|
20854
|
+
this.rawVisualUserMedia.srcObject = null;
|
|
20855
|
+
this.rawVisualUserMedia.removeAttribute("src");
|
|
20856
|
+
}
|
|
20857
|
+
this.currentVisualStream = void 0;
|
|
20858
|
+
}
|
|
20859
|
+
}
|
|
20860
|
+
async restorePlayback() {
|
|
20861
|
+
if (!this.rawVisualUserMedia) throw new Error("No camera preview is available.");
|
|
20862
|
+
await this.rawVisualUserMedia.play();
|
|
20863
|
+
await this.audioRecorder?.resume();
|
|
20864
|
+
}
|
|
20865
|
+
hasLiveStream() {
|
|
20866
|
+
const videoReady = this.currentVisualStream?.getVideoTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
|
|
20867
|
+
const audioReady = !isAudioEnabled(this.options) || this.currentVisualStream?.getAudioTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
|
|
20868
|
+
return Boolean(this.currentVisualStream?.active && videoReady && audioReady);
|
|
20869
|
+
}
|
|
20870
|
+
hasEnded() {
|
|
20871
|
+
if (this.rawVisualUserMedia?.ended) return this.rawVisualUserMedia.ended;
|
|
20872
|
+
return !this.currentVisualStream?.active;
|
|
20873
|
+
}
|
|
20874
|
+
hasInvalidDimensions() {
|
|
20875
|
+
if (!this.rawVisualUserMedia) return false;
|
|
20876
|
+
const { videoWidth, videoHeight } = this.rawVisualUserMedia;
|
|
20877
|
+
return videoWidth > 0 && videoWidth < 3 || videoHeight > 0 && videoHeight < 3;
|
|
20878
|
+
}
|
|
20879
|
+
logEvent(eventType, params) {
|
|
20880
|
+
this.options.logger.debug(`UserMedia: ... ${EVENT_ASCII} event ${eventType}: ${string_pretty(params)}`);
|
|
20881
|
+
}
|
|
20882
|
+
unloadRemainingEventListeners() {
|
|
20883
|
+
this.options.logger.debug("UserMedia: unloadRemainingEventListeners()");
|
|
20884
|
+
this.clearInitializationListeners?.();
|
|
20885
|
+
mediaEvents.forEach((eventName)=>{
|
|
20886
|
+
this.rawVisualUserMedia?.removeEventListener(eventName, this.outputEvent);
|
|
20887
|
+
});
|
|
20888
|
+
this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
|
|
20889
|
+
}
|
|
20890
|
+
audioRecord(audioCallback) {
|
|
20891
|
+
this.removeListener("SENDING_FIRST_FRAME");
|
|
20892
|
+
this.audioRecorder?.record(audioCallback);
|
|
20893
|
+
}
|
|
20894
|
+
init(localMediaStream, videoCallback, audioCallback, endedEarlyCallback, params) {
|
|
20895
|
+
this.clearInitializationListeners?.();
|
|
20896
|
+
this.stop(localMediaStream, {
|
|
20897
|
+
aboutToInitialize: true,
|
|
20898
|
+
preserveRecordingState: Boolean(params?.switchingFacingMode || params?.recovering)
|
|
20899
|
+
});
|
|
20900
|
+
const initializationId = ++this.initializationId;
|
|
20901
|
+
this.onPlayReached = false;
|
|
20902
|
+
this.onLoadedMetaDataReached = false;
|
|
20903
|
+
this.playingPromiseReached = false;
|
|
20904
|
+
this.videoTrackLabel = void 0;
|
|
20905
|
+
if (isAudioEnabled(this.options)) this.audioRecorder ??= new media_AudioRecorder(this, this.options);
|
|
20906
|
+
const unloadAllEventListeners = ()=>{
|
|
20907
|
+
this.options.logger.debug("UserMedia: unloadAllEventListeners()");
|
|
20908
|
+
this.unloadRemainingEventListeners();
|
|
20909
|
+
this.removeListener("SENDING_FIRST_FRAME");
|
|
20910
|
+
this.rawVisualUserMedia?.removeEventListener("play", onPlay);
|
|
20911
|
+
this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
|
|
20912
|
+
};
|
|
20913
|
+
this.clearInitializationListeners = ()=>{
|
|
20914
|
+
this.rawVisualUserMedia?.removeEventListener("play", onPlay);
|
|
20915
|
+
this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
|
|
20916
|
+
this.clearInitializationListeners = void 0;
|
|
20917
|
+
};
|
|
20918
|
+
const play = ()=>{
|
|
20919
|
+
try {
|
|
20920
|
+
this.rawVisualUserMedia?.load();
|
|
20921
|
+
if (this.rawVisualUserMedia?.paused) {
|
|
20922
|
+
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)}`);
|
|
20923
|
+
this.rawVisualUserMedia.play().then(()=>{
|
|
20924
|
+
if (initializationId !== this.initializationId) return;
|
|
20925
|
+
if (!this.playingPromiseReached) {
|
|
20926
|
+
this.options.logger.debug("UserMedia: play promise successful. Playing now.");
|
|
20927
|
+
this.playingPromiseReached = true;
|
|
20928
|
+
}
|
|
20929
|
+
}).catch((exc)=>{
|
|
20930
|
+
if (initializationId !== this.initializationId) return;
|
|
20931
|
+
if (exc instanceof Error && !params?.recovering) this.options.logger.warn(`Caught pending user media promise exception: ${exc.toString()}`);
|
|
20932
|
+
else {
|
|
20933
|
+
unloadAllEventListeners();
|
|
20934
|
+
endedEarlyCallback(error_createError({
|
|
20935
|
+
message: "Failed to play user media upon play event.",
|
|
20936
|
+
exc,
|
|
20937
|
+
options: this.options
|
|
20938
|
+
}));
|
|
20939
|
+
}
|
|
20940
|
+
});
|
|
20941
|
+
}
|
|
20942
|
+
} catch (exc) {
|
|
20943
|
+
unloadAllEventListeners();
|
|
20944
|
+
endedEarlyCallback(exc);
|
|
20945
|
+
}
|
|
20946
|
+
};
|
|
20947
|
+
const fireCallbacks = ()=>{
|
|
20948
|
+
const readyState = this.rawVisualUserMedia?.readyState;
|
|
20949
|
+
this.options.logger.debug(`UserMedia: fireCallbacks(readyState=${readyState}, onPlayReached=${this.onPlayReached}, onLoadedMetaDataReached=${this.onLoadedMetaDataReached})`);
|
|
20950
|
+
if (this.onPlayReached && this.onLoadedMetaDataReached) if (this.audioRecorder) try {
|
|
20951
|
+
this.audioRecorder.init(localMediaStream);
|
|
20952
|
+
const onAudioReady = ()=>{
|
|
20953
|
+
if (initializationId !== this.initializationId || this.currentVisualStream !== localMediaStream) return;
|
|
20954
|
+
if (this.recording) this.audioRecord(audioCallback);
|
|
20955
|
+
else this.on("SENDING_FIRST_FRAME", ()=>{
|
|
20956
|
+
this.audioRecord(audioCallback);
|
|
20957
|
+
});
|
|
20958
|
+
this.clearInitializationListeners?.();
|
|
20959
|
+
videoCallback();
|
|
20960
|
+
};
|
|
20961
|
+
if (params?.recovering) this.audioRecorder.resume().then(onAudioReady).catch(endedEarlyCallback);
|
|
20962
|
+
else onAudioReady();
|
|
20963
|
+
} catch (exc) {
|
|
20964
|
+
unloadAllEventListeners();
|
|
20965
|
+
this.stop(void 0, {
|
|
20966
|
+
preserveRecordingState: Boolean(params?.recovering)
|
|
20967
|
+
});
|
|
20968
|
+
endedEarlyCallback(exc);
|
|
20969
|
+
}
|
|
20970
|
+
else {
|
|
20971
|
+
this.clearInitializationListeners?.();
|
|
20972
|
+
videoCallback();
|
|
20973
|
+
}
|
|
20974
|
+
};
|
|
20975
|
+
const onPlay = ()=>{
|
|
20976
|
+
try {
|
|
20977
|
+
this.logEvent("play", {
|
|
20978
|
+
readyState: this.rawVisualUserMedia?.readyState,
|
|
20979
|
+
audio: isAudioEnabled(this.options),
|
|
20980
|
+
width: this.rawVisualUserMedia?.width,
|
|
20981
|
+
height: this.rawVisualUserMedia?.height,
|
|
20982
|
+
videoWidth: this.rawVisualUserMedia?.videoWidth,
|
|
20983
|
+
videoHeight: this.rawVisualUserMedia?.videoHeight
|
|
20984
|
+
});
|
|
20985
|
+
this.rawVisualUserMedia?.removeEventListener("play", onPlay);
|
|
20986
|
+
if (this.hasEnded() || this.hasInvalidDimensions()) endedEarlyCallback(error_createError({
|
|
20987
|
+
message: "Already busy",
|
|
20988
|
+
explanation: "Probably another browser window is using your webcam?",
|
|
20989
|
+
options: this.options
|
|
20990
|
+
}));
|
|
20991
|
+
else {
|
|
20992
|
+
this.onPlayReached = true;
|
|
20993
|
+
fireCallbacks();
|
|
20498
20994
|
}
|
|
20499
|
-
}
|
|
20500
|
-
|
|
20995
|
+
} catch (exc) {
|
|
20996
|
+
unloadAllEventListeners();
|
|
20997
|
+
endedEarlyCallback(exc);
|
|
20998
|
+
}
|
|
20501
20999
|
};
|
|
20502
|
-
|
|
20503
|
-
|
|
20504
|
-
|
|
20505
|
-
|
|
20506
|
-
|
|
20507
|
-
|
|
20508
|
-
|
|
21000
|
+
const onLoadedMetaData = ()=>{
|
|
21001
|
+
this.logEvent("loadedmetadata", {
|
|
21002
|
+
readyState: this.rawVisualUserMedia?.readyState,
|
|
21003
|
+
paused: this.rawVisualUserMedia?.paused,
|
|
21004
|
+
width: this.rawVisualUserMedia?.width,
|
|
21005
|
+
height: this.rawVisualUserMedia?.height,
|
|
21006
|
+
videoWidth: this.rawVisualUserMedia?.videoWidth,
|
|
21007
|
+
videoHeight: this.rawVisualUserMedia?.videoHeight
|
|
20509
21008
|
});
|
|
20510
|
-
this.
|
|
20511
|
-
|
|
20512
|
-
|
|
20513
|
-
|
|
20514
|
-
|
|
20515
|
-
|
|
20516
|
-
const videoConstraints = "object" == typeof constraints.video ? constraints.video : void 0;
|
|
20517
|
-
if (!videoConstraints) return;
|
|
20518
|
-
if (params?.switchingFacingMode || params?.recovering) videoConstraints.facingMode = params.switchingFacingMode || this.facingMode;
|
|
20519
|
-
if (this.options.video.width) videoConstraints.width = {
|
|
20520
|
-
ideal: this.options.video.width
|
|
20521
|
-
};
|
|
20522
|
-
else {
|
|
20523
|
-
const limitedWidth = this.limitWidth();
|
|
20524
|
-
if (limitedWidth?.value) videoConstraints.width = {
|
|
20525
|
-
ideal: limitedWidth.value
|
|
20526
|
-
};
|
|
20527
|
-
}
|
|
20528
|
-
if (this.options.video.height) videoConstraints.height = {
|
|
20529
|
-
ideal: this.options.video.height
|
|
21009
|
+
this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
|
|
21010
|
+
if (!this.hasEnded() && !this.hasInvalidDimensions()) {
|
|
21011
|
+
this.emit("LOADED_META_DATA");
|
|
21012
|
+
this.onLoadedMetaDataReached = true;
|
|
21013
|
+
fireCallbacks();
|
|
21014
|
+
}
|
|
20530
21015
|
};
|
|
20531
|
-
}
|
|
20532
|
-
loadUserMedia(params) {
|
|
20533
|
-
if (this.userMediaLoaded) {
|
|
20534
|
-
this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already loaded");
|
|
20535
|
-
this.onUserMediaReady(params);
|
|
20536
|
-
return;
|
|
20537
|
-
} else if (this.userMediaLoading) return void this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already asking for permission");
|
|
20538
|
-
this.options.logger.debug(`Recorder: loadUserMedia(${params ? string_pretty(params) : ""})`);
|
|
20539
|
-
this.emit("LOADING_USER_MEDIA");
|
|
20540
21016
|
try {
|
|
20541
|
-
|
|
20542
|
-
this.
|
|
20543
|
-
|
|
20544
|
-
|
|
20545
|
-
|
|
20546
|
-
|
|
20547
|
-
audio: isAudioEnabled(this.options)
|
|
20548
|
-
});
|
|
20549
|
-
}
|
|
20550
|
-
}, this.options.timeouts.userMedia);
|
|
20551
|
-
this.userMediaLoading = true;
|
|
20552
|
-
this.loadGenuineUserMedia(params);
|
|
20553
|
-
} catch (exc) {
|
|
20554
|
-
this.clearUserMediaTimeout();
|
|
20555
|
-
this.options.logger.debug("Recorder: failed to load genuine user media");
|
|
20556
|
-
this.userMediaLoading = false;
|
|
20557
|
-
if (this.cameraRecovery) return void this.failCameraRecovery(exc);
|
|
20558
|
-
const errorListeners = this.getListeners("ERROR");
|
|
20559
|
-
if (errorListeners?.length) this.emit("ERROR", {
|
|
20560
|
-
exc
|
|
21017
|
+
const videoTrack = media_getFirstVideoTrack(localMediaStream);
|
|
21018
|
+
this.videoTrackLabel = videoTrack?.label;
|
|
21019
|
+
if (videoTrack) if (videoTrack.enabled) if (util_isVirtualCamera(videoTrack)) throw error_createError({
|
|
21020
|
+
message: "Virtual cameras are not allowed.",
|
|
21021
|
+
explanation: `Please use a real camera, not "${videoTrack.label}".`,
|
|
21022
|
+
options: this.options
|
|
20561
21023
|
});
|
|
20562
21024
|
else {
|
|
20563
|
-
|
|
20564
|
-
|
|
20565
|
-
|
|
20566
|
-
|
|
20567
|
-
}
|
|
20568
|
-
executeCommand(command) {
|
|
20569
|
-
if (this.unloaded) return;
|
|
20570
|
-
try {
|
|
20571
|
-
if (command.args) {
|
|
20572
|
-
const debugLine = string_summarize(`Server commanded: ${command.command} with:`, command.args);
|
|
20573
|
-
this.options.logger.debug(debugLine);
|
|
20574
|
-
} else this.options.logger.debug(`Server commanded: ${command.command}`);
|
|
20575
|
-
switch(command.command){
|
|
20576
|
-
case "ready":
|
|
20577
|
-
this.emit("SERVER_READY");
|
|
20578
|
-
if (!this.userMediaTimeout) if (this.options.loadUserMediaOnRecord) this.show();
|
|
20579
|
-
else this.loadUserMedia();
|
|
20580
|
-
break;
|
|
20581
|
-
case "preview":
|
|
20582
|
-
this.preview(command.args);
|
|
20583
|
-
break;
|
|
20584
|
-
case "error":
|
|
20585
|
-
{
|
|
20586
|
-
let explanation = "(No explanation given)";
|
|
20587
|
-
if (command.args?.err?.message) explanation = command.args.err.message;
|
|
20588
|
-
const err = error_createError({
|
|
20589
|
-
message: "Websocket error from Videomail server",
|
|
20590
|
-
explanation,
|
|
20591
|
-
err: deserializeError(command.args?.err),
|
|
20592
|
-
options: this.options
|
|
20593
|
-
});
|
|
20594
|
-
this.emit("ERROR", {
|
|
20595
|
-
err
|
|
20596
|
-
});
|
|
20597
|
-
break;
|
|
20598
|
-
}
|
|
20599
|
-
case "confirmFrame":
|
|
20600
|
-
this.updateFrameProgress(command.args);
|
|
20601
|
-
break;
|
|
20602
|
-
case "confirmSample":
|
|
20603
|
-
this.updateSampleProgress(command.args);
|
|
20604
|
-
break;
|
|
20605
|
-
case "beginAudioEncoding":
|
|
20606
|
-
this.emit("BEGIN_AUDIO_ENCODING");
|
|
20607
|
-
break;
|
|
20608
|
-
case "beginVideoEncoding":
|
|
20609
|
-
this.emit("BEGIN_VIDEO_ENCODING");
|
|
20610
|
-
break;
|
|
20611
|
-
default:
|
|
20612
|
-
{
|
|
20613
|
-
const err = error_createError({
|
|
20614
|
-
message: `Unknown server command: ${command.command}`,
|
|
20615
|
-
options: this.options
|
|
20616
|
-
});
|
|
20617
|
-
this.emit("ERROR", {
|
|
20618
|
-
err
|
|
20619
|
-
});
|
|
20620
|
-
break;
|
|
20621
|
-
}
|
|
20622
|
-
}
|
|
20623
|
-
} catch (exc) {
|
|
20624
|
-
this.emit("ERROR", {
|
|
20625
|
-
exc
|
|
20626
|
-
});
|
|
20627
|
-
}
|
|
20628
|
-
}
|
|
20629
|
-
isNotifying() {
|
|
20630
|
-
return this.visuals.isNotifying();
|
|
20631
|
-
}
|
|
20632
|
-
isHidden() {
|
|
20633
|
-
return !this.recorderElement || html_isHidden(this.recorderElement);
|
|
20634
|
-
}
|
|
20635
|
-
writeCommand(command, args, cb) {
|
|
20636
|
-
if (this.connected) {
|
|
20637
|
-
if (this.stream) {
|
|
20638
|
-
if (args) this.options.logger.debug(`$ ${command} with ${string_pretty(args)}`);
|
|
20639
|
-
else this.options.logger.debug(`$ ${command}`);
|
|
20640
|
-
const commandObj = {
|
|
20641
|
-
command,
|
|
20642
|
-
args
|
|
20643
|
-
};
|
|
20644
|
-
this.writeStream(Buffer.from(JSON.stringify(commandObj)), cb ? {
|
|
20645
|
-
onFlushedCallback: ()=>{
|
|
20646
|
-
queueMicrotask(cb);
|
|
20647
|
-
}
|
|
20648
|
-
} : void 0);
|
|
21025
|
+
let description = "";
|
|
21026
|
+
if (videoTrack.label.length > 0) description = description.concat(videoTrack.label);
|
|
21027
|
+
description = description.concat(` with enabled=${videoTrack.enabled}, muted=${videoTrack.muted}, readyState=${videoTrack.readyState}`);
|
|
21028
|
+
this.options.logger.debug(`UserMedia: ${videoTrack.kind} detected. ${description}`);
|
|
20649
21029
|
}
|
|
20650
|
-
|
|
20651
|
-
|
|
20652
|
-
|
|
20653
|
-
this.
|
|
21030
|
+
else throw error_createError({
|
|
21031
|
+
message: "Webcam is disabled",
|
|
21032
|
+
explanation: "The video track seems to be disabled. Enable it in your system.",
|
|
21033
|
+
options: this.options
|
|
20654
21034
|
});
|
|
21035
|
+
else this.options.logger.debug("UserMedia: detected (but no video tracks exist");
|
|
21036
|
+
this.rawVisualUserMedia?.addEventListener("loadedmetadata", onLoadedMetaData);
|
|
21037
|
+
this.rawVisualUserMedia?.addEventListener("play", onPlay);
|
|
21038
|
+
this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
|
|
21039
|
+
this.rawVisualUserMedia?.addEventListener("error", this.handleMediaError);
|
|
21040
|
+
this.setVisualStream(localMediaStream);
|
|
21041
|
+
play();
|
|
21042
|
+
} catch (exc) {
|
|
21043
|
+
unloadAllEventListeners();
|
|
21044
|
+
endedEarlyCallback(exc);
|
|
20655
21045
|
}
|
|
20656
21046
|
}
|
|
20657
|
-
|
|
20658
|
-
this.
|
|
20659
|
-
}
|
|
20660
|
-
getIntervalSum() {
|
|
20661
|
-
if (!this.loop) return;
|
|
20662
|
-
return this.loop.getElapsedTime();
|
|
20663
|
-
}
|
|
20664
|
-
getAvgInterval() {
|
|
20665
|
-
const intervalSum = this.getIntervalSum() ?? 0;
|
|
20666
|
-
if (!this.hasValidInterval(intervalSum)) return;
|
|
20667
|
-
return intervalSum / this.framesCount;
|
|
20668
|
-
}
|
|
20669
|
-
getAvgFps() {
|
|
20670
|
-
const intervalSum = this.getIntervalSum() ?? 0;
|
|
20671
|
-
if (!this.hasValidInterval(intervalSum)) return;
|
|
20672
|
-
return this.framesCount / intervalSum * 1000;
|
|
20673
|
-
}
|
|
20674
|
-
hasValidInterval(intervalSum) {
|
|
20675
|
-
return intervalSum > 0;
|
|
20676
|
-
}
|
|
20677
|
-
getRecordingStats() {
|
|
20678
|
-
return this.recordingStats;
|
|
21047
|
+
getVideoTrackLabel() {
|
|
21048
|
+
return this.videoTrackLabel;
|
|
20679
21049
|
}
|
|
20680
|
-
|
|
20681
|
-
return this.
|
|
21050
|
+
isReady() {
|
|
21051
|
+
return Boolean(this.rawVisualUserMedia?.srcObject || this.rawVisualUserMedia?.currentSrc);
|
|
20682
21052
|
}
|
|
20683
|
-
stop(params) {
|
|
20684
|
-
|
|
20685
|
-
|
|
20686
|
-
|
|
20687
|
-
|
|
20688
|
-
|
|
20689
|
-
|
|
20690
|
-
|
|
20691
|
-
|
|
20692
|
-
|
|
20693
|
-
this.stopTimeout = window.setTimeout(()=>{
|
|
20694
|
-
this.stopTimeout = void 0;
|
|
20695
|
-
this.stopTime = Date.now();
|
|
20696
|
-
const videoType = this.replay.getVideoType();
|
|
20697
|
-
if (!videoType) throw new Error("Unable to video record when no video type is defined.");
|
|
20698
|
-
this.recordingStats = {
|
|
20699
|
-
avgFps: this.getAvgFps(),
|
|
20700
|
-
wantedFps: this.options.video.fps,
|
|
20701
|
-
avgInterval: this.getAvgInterval(),
|
|
20702
|
-
wantedInterval: 1e3 / this.options.video.fps,
|
|
20703
|
-
intervalSum: this.getIntervalSum(),
|
|
20704
|
-
framesCount: this.framesCount,
|
|
20705
|
-
videoType
|
|
20706
|
-
};
|
|
20707
|
-
if (this.userMedia) this.recordingStats.videoTrackLabel = this.userMedia.getVideoTrackLabel();
|
|
20708
|
-
if (isAudioEnabled(this.options) && this.userMedia) {
|
|
20709
|
-
this.recordingStats.samplesCount = this.samplesCount;
|
|
20710
|
-
this.recordingStats.sampleRate = this.recordingAudioSampleRate ?? this.userMedia.getAudioSampleRate();
|
|
20711
|
-
}
|
|
20712
|
-
this.writeCommand("stop", this.recordingStats, ()=>{
|
|
20713
|
-
this.emit("STOPPED", {
|
|
20714
|
-
recordingStats: this.recordingStats
|
|
21053
|
+
stop(visualStream, params) {
|
|
21054
|
+
try {
|
|
21055
|
+
let chosenStream = visualStream;
|
|
21056
|
+
if (!params?.aboutToInitialize) {
|
|
21057
|
+
this.initializationId++;
|
|
21058
|
+
this.clearInitializationListeners?.();
|
|
21059
|
+
chosenStream ??= this.currentVisualStream;
|
|
21060
|
+
const tracks = chosenStream?.getTracks();
|
|
21061
|
+
if (tracks) tracks.forEach((track)=>{
|
|
21062
|
+
track.stop();
|
|
20715
21063
|
});
|
|
21064
|
+
this.setVisualStream();
|
|
21065
|
+
this.audioRecorder?.stop();
|
|
21066
|
+
this.audioRecorder = void 0;
|
|
21067
|
+
}
|
|
21068
|
+
if (!params?.preserveRecordingState) this.paused = this.recording = false;
|
|
21069
|
+
} catch (exc) {
|
|
21070
|
+
this.emit("ERROR", {
|
|
21071
|
+
exc
|
|
20716
21072
|
});
|
|
20717
|
-
|
|
20718
|
-
}, 60);
|
|
21073
|
+
}
|
|
20719
21074
|
}
|
|
20720
|
-
|
|
20721
|
-
|
|
20722
|
-
|
|
20723
|
-
|
|
20724
|
-
|
|
20725
|
-
|
|
20726
|
-
|
|
21075
|
+
createCanvas() {
|
|
21076
|
+
const canvas = document.createElement("canvas");
|
|
21077
|
+
const rawWidthDimension = this.getRawWidth(true);
|
|
21078
|
+
if (rawWidthDimension.value) canvas.width = rawWidthDimension.value;
|
|
21079
|
+
const rawHeight = this.getRawHeight(true);
|
|
21080
|
+
if (rawHeight) canvas.height = rawHeight;
|
|
21081
|
+
return canvas;
|
|
20727
21082
|
}
|
|
20728
|
-
|
|
20729
|
-
this.
|
|
20730
|
-
this.
|
|
20731
|
-
this.userMediaLoading = false;
|
|
20732
|
-
this.userMedia?.stop();
|
|
20733
|
-
this.recordingDimensions = void 0;
|
|
20734
|
-
this.userMediaLoaded = this.key = this.canvas = this.ctx = void 0;
|
|
20735
|
-
this.loadUserMedia();
|
|
21083
|
+
getVideoHeight() {
|
|
21084
|
+
if (!this.rawVisualUserMedia) return;
|
|
21085
|
+
return this.rawVisualUserMedia.videoHeight || this.rawVisualUserMedia.height;
|
|
20736
21086
|
}
|
|
20737
|
-
|
|
20738
|
-
if (
|
|
20739
|
-
|
|
20740
|
-
let cause;
|
|
20741
|
-
if (e) cause = e.type;
|
|
20742
|
-
const prettyCause = cause ? `, cause: ${cause}` : "";
|
|
20743
|
-
this.options.logger.debug(`Recorder: unload()${prettyCause}`);
|
|
20744
|
-
this.recordingActive = false;
|
|
20745
|
-
this.mediaTransmissionStarted = false;
|
|
20746
|
-
this.reset();
|
|
20747
|
-
this.recordingDimensions = void 0;
|
|
20748
|
-
this.clearUserMediaTimeout();
|
|
20749
|
-
this.clearConnectionTimeout();
|
|
20750
|
-
this.clearReconnectTimeout();
|
|
20751
|
-
this.clearRetryTimeout();
|
|
20752
|
-
this.clearStopTimeout();
|
|
20753
|
-
this.stopPings();
|
|
20754
|
-
this.userMediaLoading = false;
|
|
20755
|
-
this.connecting = false;
|
|
20756
|
-
this.reconnecting = false;
|
|
20757
|
-
this.reconnectAttempts = 0;
|
|
20758
|
-
if (this.userMedia) this.userMedia.unloadRemainingEventListeners();
|
|
20759
|
-
if (this.submitting) ;
|
|
20760
|
-
else if (this.stream) this.discardSocket();
|
|
20761
|
-
this.unloaded = true;
|
|
20762
|
-
this.built = this.connecting = this.connected = false;
|
|
21087
|
+
getVideoWidth() {
|
|
21088
|
+
if (!this.rawVisualUserMedia) return;
|
|
21089
|
+
return this.rawVisualUserMedia.videoWidth || this.rawVisualUserMedia.width;
|
|
20763
21090
|
}
|
|
20764
|
-
|
|
20765
|
-
this.
|
|
20766
|
-
|
|
20767
|
-
this.options.logger.debug("Recorder: reset()");
|
|
20768
|
-
this.emit("RESETTING");
|
|
20769
|
-
this.cancelAnimationFrame();
|
|
20770
|
-
this.userMedia?.stop();
|
|
20771
|
-
this.replay.reset();
|
|
20772
|
-
this.userMediaLoaded = this.key = this.canvas = this.ctx = this.recordingBuffer = void 0;
|
|
20773
|
-
}
|
|
21091
|
+
hasVideoWidth() {
|
|
21092
|
+
const videoWidth = this.getVideoWidth();
|
|
21093
|
+
return videoWidth && videoWidth > 0;
|
|
20774
21094
|
}
|
|
20775
|
-
|
|
20776
|
-
|
|
20777
|
-
|
|
20778
|
-
|
|
20779
|
-
|
|
21095
|
+
getRawWidth(responsive) {
|
|
21096
|
+
let rawWidth;
|
|
21097
|
+
const widthDimension = {
|
|
21098
|
+
unit: "px"
|
|
21099
|
+
};
|
|
21100
|
+
rawWidth = responsive ? this.getVideoWidth() : this.options.video.width ? this.options.video.width : this.getVideoWidth();
|
|
21101
|
+
if (responsive) {
|
|
21102
|
+
const widthDimension = this.recorder.limitWidth(rawWidth);
|
|
21103
|
+
rawWidth = widthDimension?.value;
|
|
20780
21104
|
}
|
|
21105
|
+
widthDimension.value = rawWidth;
|
|
21106
|
+
return widthDimension;
|
|
20781
21107
|
}
|
|
20782
|
-
|
|
20783
|
-
|
|
20784
|
-
|
|
20785
|
-
|
|
21108
|
+
getRawHeight(responsive) {
|
|
21109
|
+
let rawHeight;
|
|
21110
|
+
if (responsive) {
|
|
21111
|
+
rawHeight = this.options.video.height || this.getVideoHeight();
|
|
21112
|
+
const ratio = this.recorder.getRatio();
|
|
21113
|
+
const rawWidth = this.getRawWidth(responsive);
|
|
21114
|
+
if (void 0 !== ratio && rawWidth.value) rawHeight = rawWidth.value * ratio;
|
|
21115
|
+
} else rawHeight = this.options.video.height ? this.options.video.height : this.getVideoHeight();
|
|
21116
|
+
if (responsive) {
|
|
21117
|
+
const heightDimension = this.recorder.limitHeight(rawHeight);
|
|
21118
|
+
rawHeight = heightDimension.value;
|
|
21119
|
+
}
|
|
21120
|
+
return rawHeight;
|
|
20786
21121
|
}
|
|
20787
|
-
|
|
20788
|
-
return
|
|
21122
|
+
getRawVisuals() {
|
|
21123
|
+
return this.rawVisualUserMedia;
|
|
20789
21124
|
}
|
|
20790
|
-
|
|
20791
|
-
|
|
21125
|
+
pause() {
|
|
21126
|
+
this.paused = true;
|
|
20792
21127
|
}
|
|
20793
|
-
|
|
20794
|
-
this.
|
|
20795
|
-
const resolve = this.resolveCameraRecovery;
|
|
20796
|
-
const resumeRequested = restored && this.resumeAfterCameraRecovery;
|
|
20797
|
-
this.cameraRecovery = void 0;
|
|
20798
|
-
this.resolveCameraRecovery = void 0;
|
|
20799
|
-
this.resumeAfterCameraRecovery = false;
|
|
20800
|
-
resolve?.(restored);
|
|
20801
|
-
if (resumeRequested && this.isPaused()) this.resume();
|
|
21128
|
+
isPaused() {
|
|
21129
|
+
return this.paused;
|
|
20802
21130
|
}
|
|
20803
|
-
|
|
20804
|
-
|
|
20805
|
-
this.mediaRequestId++;
|
|
20806
|
-
this.userMediaLoading = false;
|
|
20807
|
-
this.userMediaLoaded = false;
|
|
20808
|
-
this.clearUserMediaTimeout();
|
|
20809
|
-
this.userMedia?.stop(void 0, {
|
|
20810
|
-
preserveRecordingState: true
|
|
20811
|
-
});
|
|
20812
|
-
this.finishCameraRecovery(false);
|
|
21131
|
+
resume() {
|
|
21132
|
+
this.paused = false;
|
|
20813
21133
|
}
|
|
20814
|
-
|
|
20815
|
-
this.
|
|
20816
|
-
const err = error_createError({
|
|
20817
|
-
message: "Camera recovery failed",
|
|
20818
|
-
explanation: "Your recording is still paused. Check camera permissions, then try Resume again or stop to keep the recorded part.",
|
|
20819
|
-
exc,
|
|
20820
|
-
options: this.options
|
|
20821
|
-
});
|
|
20822
|
-
this.emit("CAMERA_RECOVERY_FAILED", {
|
|
20823
|
-
err
|
|
20824
|
-
});
|
|
21134
|
+
record() {
|
|
21135
|
+
this.recording = true;
|
|
20825
21136
|
}
|
|
20826
|
-
|
|
20827
|
-
|
|
20828
|
-
if (!this.built || this.unloaded || !this.userMedia || this.userMediaLoading || this.submitting || this.key || void 0 !== this.stopTimeout || this.recordingTransportFailed) return Promise.resolve(false);
|
|
20829
|
-
const liveStream = this.userMedia.hasLiveStream();
|
|
20830
|
-
if (liveStream && this.isRecording()) return Promise.resolve(true);
|
|
20831
|
-
if (!liveStream && this.recordingActive && !this.isPaused()) this.pause();
|
|
20832
|
-
const recovery = new Promise((resolve)=>{
|
|
20833
|
-
this.resolveCameraRecovery = resolve;
|
|
20834
|
-
});
|
|
20835
|
-
this.cameraRecovery = recovery;
|
|
20836
|
-
this.emit("CAMERA_RECOVERING");
|
|
20837
|
-
if (liveStream) {
|
|
20838
|
-
this.userMediaTimeout = window.setTimeout(()=>{
|
|
20839
|
-
this.failCameraRecovery(new Error("Camera playback recovery timed out."));
|
|
20840
|
-
}, this.options.timeouts.userMedia);
|
|
20841
|
-
this.userMedia.restorePlayback().then(()=>{
|
|
20842
|
-
if (this.cameraRecovery === recovery) this.finishCameraRecovery(true);
|
|
20843
|
-
}).catch((exc)=>{
|
|
20844
|
-
if (this.cameraRecovery === recovery) this.failCameraRecovery(exc);
|
|
20845
|
-
});
|
|
20846
|
-
} else {
|
|
20847
|
-
this.userMediaLoaded = false;
|
|
20848
|
-
this.loadUserMedia({
|
|
20849
|
-
recovering: true
|
|
20850
|
-
});
|
|
20851
|
-
}
|
|
20852
|
-
return recovery;
|
|
21137
|
+
isRecording() {
|
|
21138
|
+
return this.recording;
|
|
20853
21139
|
}
|
|
20854
|
-
|
|
20855
|
-
this.
|
|
20856
|
-
|
|
20857
|
-
if (params) this.options.logger.debug(`pause() at frame ${this.framesCount} with ${string_pretty(params)}`);
|
|
20858
|
-
else this.options.logger.debug(`pause() at frame ${this.framesCount}`);
|
|
20859
|
-
this.userMedia?.pause();
|
|
20860
|
-
this.loop.stop();
|
|
20861
|
-
this.emit("PAUSED");
|
|
20862
|
-
this.sendPings();
|
|
21140
|
+
getAudioSampleRate() {
|
|
21141
|
+
if (this.audioRecorder) return this.audioRecorder.getSampleRate();
|
|
21142
|
+
return -1;
|
|
20863
21143
|
}
|
|
20864
|
-
|
|
20865
|
-
|
|
20866
|
-
this.
|
|
20867
|
-
|
|
20868
|
-
|
|
20869
|
-
|
|
20870
|
-
|
|
20871
|
-
|
|
20872
|
-
|
|
20873
|
-
this.userMedia.resume();
|
|
20874
|
-
if (this.streamBackpressured) this.resumeAfterDrain = true;
|
|
20875
|
-
else this.loop.start();
|
|
21144
|
+
getCharacteristics() {
|
|
21145
|
+
return {
|
|
21146
|
+
audioSampleRate: this.getAudioSampleRate(),
|
|
21147
|
+
muted: this.rawVisualUserMedia?.muted,
|
|
21148
|
+
width: this.rawVisualUserMedia?.width,
|
|
21149
|
+
height: this.rawVisualUserMedia?.height,
|
|
21150
|
+
videoWidth: this.rawVisualUserMedia?.videoWidth,
|
|
21151
|
+
videoHeight: this.rawVisualUserMedia?.videoHeight
|
|
21152
|
+
};
|
|
20876
21153
|
}
|
|
20877
|
-
|
|
20878
|
-
|
|
20879
|
-
|
|
21154
|
+
}
|
|
21155
|
+
const visuals_userMedia = UserMedia;
|
|
21156
|
+
class Recorder extends capture {
|
|
21157
|
+
dimensions;
|
|
21158
|
+
constructor(visuals, replay, options){
|
|
21159
|
+
super(visuals, replay, options);
|
|
21160
|
+
this.dimensions = new recorder_dimensions({
|
|
21161
|
+
options,
|
|
21162
|
+
visuals,
|
|
21163
|
+
getRecorderElement: ()=>this.recorderElement,
|
|
21164
|
+
getUserMedia: ()=>this.userMedia
|
|
21165
|
+
});
|
|
20880
21166
|
}
|
|
20881
|
-
|
|
20882
|
-
if (this.
|
|
20883
|
-
if (!this.userMedia) throw new Error("No user media defined, unable to draw on canvas");
|
|
21167
|
+
executeCommand(command) {
|
|
21168
|
+
if (this.unloaded) return;
|
|
20884
21169
|
try {
|
|
20885
|
-
if (
|
|
20886
|
-
|
|
20887
|
-
this.
|
|
20888
|
-
|
|
20889
|
-
|
|
20890
|
-
|
|
20891
|
-
|
|
20892
|
-
|
|
20893
|
-
|
|
20894
|
-
|
|
20895
|
-
|
|
20896
|
-
|
|
20897
|
-
|
|
20898
|
-
|
|
20899
|
-
|
|
20900
|
-
|
|
20901
|
-
|
|
20902
|
-
|
|
20903
|
-
|
|
20904
|
-
|
|
20905
|
-
|
|
21170
|
+
if (command.args) {
|
|
21171
|
+
const debugLine = string_summarize(`Server commanded: ${command.command} with:`, command.args);
|
|
21172
|
+
this.options.logger.debug(debugLine);
|
|
21173
|
+
} else this.options.logger.debug(`Server commanded: ${command.command}`);
|
|
21174
|
+
switch(command.command){
|
|
21175
|
+
case "ready":
|
|
21176
|
+
this.emit("SERVER_READY");
|
|
21177
|
+
if (!this.userMediaTimeout) if (this.options.loadUserMediaOnRecord) this.show();
|
|
21178
|
+
else this.loadUserMedia();
|
|
21179
|
+
break;
|
|
21180
|
+
case "preview":
|
|
21181
|
+
this.preview(command.args);
|
|
21182
|
+
break;
|
|
21183
|
+
case "error":
|
|
21184
|
+
{
|
|
21185
|
+
let explanation = "(No explanation given)";
|
|
21186
|
+
if (command.args?.err?.message) explanation = command.args.err.message;
|
|
21187
|
+
const err = error_createError({
|
|
21188
|
+
message: "Websocket error from Videomail server",
|
|
21189
|
+
explanation,
|
|
21190
|
+
err: deserializeError(command.args?.err),
|
|
21191
|
+
options: this.options
|
|
20906
21192
|
});
|
|
20907
|
-
this.
|
|
21193
|
+
this.emit("ERROR", {
|
|
21194
|
+
err
|
|
21195
|
+
});
|
|
21196
|
+
break;
|
|
21197
|
+
}
|
|
21198
|
+
case "confirmFrame":
|
|
21199
|
+
this.updateFrameProgress(command.args);
|
|
21200
|
+
break;
|
|
21201
|
+
case "confirmSample":
|
|
21202
|
+
this.updateSampleProgress(command.args);
|
|
21203
|
+
break;
|
|
21204
|
+
case "beginAudioEncoding":
|
|
21205
|
+
this.emit("BEGIN_AUDIO_ENCODING");
|
|
21206
|
+
break;
|
|
21207
|
+
case "beginVideoEncoding":
|
|
21208
|
+
this.emit("BEGIN_VIDEO_ENCODING");
|
|
21209
|
+
break;
|
|
21210
|
+
default:
|
|
21211
|
+
{
|
|
21212
|
+
const err = error_createError({
|
|
21213
|
+
message: `Unknown server command: ${command.command}`,
|
|
21214
|
+
options: this.options
|
|
21215
|
+
});
|
|
21216
|
+
this.emit("ERROR", {
|
|
21217
|
+
err
|
|
21218
|
+
});
|
|
21219
|
+
break;
|
|
20908
21220
|
}
|
|
20909
|
-
} else throw error_createError({
|
|
20910
|
-
message: "Failed to extract webcam data.",
|
|
20911
|
-
options: this.options
|
|
20912
|
-
});
|
|
20913
21221
|
}
|
|
20914
21222
|
} catch (exc) {
|
|
20915
21223
|
this.emit("ERROR", {
|
|
@@ -20917,156 +21225,34 @@
|
|
|
20917
21225
|
});
|
|
20918
21226
|
}
|
|
20919
21227
|
}
|
|
20920
|
-
|
|
20921
|
-
|
|
20922
|
-
|
|
20923
|
-
|
|
20924
|
-
|
|
20925
|
-
|
|
20926
|
-
|
|
20927
|
-
|
|
20928
|
-
if (this.unloaded || this.cameraRecovery) return;
|
|
20929
|
-
if (!this.connected) {
|
|
20930
|
-
this.options.logger.debug("Recorder: reconnecting before recording …");
|
|
20931
|
-
this.initSocket(()=>{
|
|
20932
|
-
this.once("SERVER_READY", ()=>{
|
|
20933
|
-
if (this.unloaded) return;
|
|
20934
|
-
if (this.userMediaLoaded || this.options.loadUserMediaOnRecord) this.record();
|
|
20935
|
-
else this.once("USER_MEDIA_READY", ()=>{
|
|
20936
|
-
if (!this.unloaded) this.record();
|
|
20937
|
-
});
|
|
20938
|
-
});
|
|
20939
|
-
});
|
|
20940
|
-
return;
|
|
20941
|
-
}
|
|
20942
|
-
if (!this.userMediaLoaded) {
|
|
20943
|
-
if (this.options.loadUserMediaOnRecord) this.loadUserMedia({
|
|
20944
|
-
recordWhenReady: true
|
|
20945
|
-
});
|
|
20946
|
-
else {
|
|
20947
|
-
const err = error_createError({
|
|
20948
|
-
message: "Load and enable your camera first",
|
|
20949
|
-
options: this.options
|
|
20950
|
-
});
|
|
20951
|
-
this.emit("ERROR", {
|
|
20952
|
-
err
|
|
20953
|
-
});
|
|
20954
|
-
}
|
|
20955
|
-
return;
|
|
20956
|
-
}
|
|
20957
|
-
this.recordingDimensions = void 0;
|
|
20958
|
-
try {
|
|
20959
|
-
if (!this.userMedia) throw new Error("No user media defined, unable to create canvas");
|
|
20960
|
-
this.canvas = this.userMedia.createCanvas();
|
|
20961
|
-
} catch (exc) {
|
|
20962
|
-
const err = error_createError({
|
|
20963
|
-
exc,
|
|
20964
|
-
options: this.options
|
|
20965
|
-
});
|
|
20966
|
-
this.emit("ERROR", {
|
|
20967
|
-
err
|
|
20968
|
-
});
|
|
20969
|
-
return;
|
|
20970
|
-
}
|
|
20971
|
-
this.ctx = this.canvas.getContext("2d");
|
|
20972
|
-
if (!this.canvas.width) {
|
|
20973
|
-
const err = error_createError({
|
|
20974
|
-
message: "Canvas has an invalid width.",
|
|
20975
|
-
options: this.options
|
|
20976
|
-
});
|
|
20977
|
-
this.emit("ERROR", {
|
|
20978
|
-
err
|
|
20979
|
-
});
|
|
20980
|
-
return;
|
|
20981
|
-
}
|
|
20982
|
-
if (!this.canvas.height) {
|
|
20983
|
-
const err = error_createError({
|
|
20984
|
-
message: "Canvas has an invalid height.",
|
|
20985
|
-
options: this.options
|
|
20986
|
-
});
|
|
20987
|
-
this.emit("ERROR", {
|
|
20988
|
-
err
|
|
20989
|
-
});
|
|
20990
|
-
return;
|
|
20991
|
-
}
|
|
20992
|
-
this.recordingDimensions = {
|
|
20993
|
-
width: this.canvas.width,
|
|
20994
|
-
height: this.canvas.height
|
|
20995
|
-
};
|
|
20996
|
-
this.frame = new canvas_to_buffer_modern_r(this.canvas, this.options.image.types, this.options.image.quality);
|
|
20997
|
-
this.options.logger.debug("Recorder: record()");
|
|
20998
|
-
this.recordingActive = true;
|
|
20999
|
-
this.recordingTransportFailed = false;
|
|
21228
|
+
unload(params) {
|
|
21229
|
+
if (this.unloaded || !this.built) return;
|
|
21230
|
+
const e = params?.e;
|
|
21231
|
+
let cause;
|
|
21232
|
+
if (e) cause = e.type;
|
|
21233
|
+
const prettyCause = cause ? `, cause: ${cause}` : "";
|
|
21234
|
+
this.options.logger.debug(`Recorder: unload()${prettyCause}`);
|
|
21235
|
+
this.recordingActive = false;
|
|
21000
21236
|
this.mediaTransmissionStarted = false;
|
|
21001
|
-
this.
|
|
21002
|
-
this.
|
|
21003
|
-
this.
|
|
21004
|
-
|
|
21005
|
-
|
|
21006
|
-
this.
|
|
21007
|
-
|
|
21008
|
-
|
|
21009
|
-
|
|
21010
|
-
|
|
21011
|
-
this.
|
|
21012
|
-
|
|
21013
|
-
|
|
21014
|
-
|
|
21015
|
-
|
|
21016
|
-
|
|
21017
|
-
this.loop.setRequestAnimationFrameObject(newObj);
|
|
21018
|
-
if (isRecording) this.loop.start();
|
|
21019
|
-
}
|
|
21020
|
-
}
|
|
21021
|
-
restoreAnimationFrameObject() {
|
|
21022
|
-
this.options.logger.debug("Recorder: restoreAnimationFrameObject()");
|
|
21023
|
-
this.setAnimationFrameObject(this.originalAnimationFrameObject);
|
|
21024
|
-
}
|
|
21025
|
-
loopWithTimeouts() {
|
|
21026
|
-
this.options.logger.debug("Recorder: loopWithTimeouts()");
|
|
21027
|
-
const wantedInterval = 1e3 / this.options.video.fps;
|
|
21028
|
-
let processingTime = 0;
|
|
21029
|
-
let start;
|
|
21030
|
-
const raf = (fn)=>setTimeout(()=>{
|
|
21031
|
-
start = Date.now();
|
|
21032
|
-
fn();
|
|
21033
|
-
processingTime = Date.now() - start;
|
|
21034
|
-
}, wantedInterval - processingTime);
|
|
21035
|
-
const cancel = (id)=>{
|
|
21036
|
-
window.clearTimeout(id);
|
|
21037
|
-
};
|
|
21038
|
-
this.setAnimationFrameObject({
|
|
21039
|
-
requestAnimationFrame: raf,
|
|
21040
|
-
cancelAnimationFrame: cancel
|
|
21041
|
-
});
|
|
21237
|
+
this.reset();
|
|
21238
|
+
this.recordingDimensions = void 0;
|
|
21239
|
+
this.clearUserMediaTimeout();
|
|
21240
|
+
this.clearConnectionTimeout();
|
|
21241
|
+
this.clearReconnectTimeout();
|
|
21242
|
+
this.clearRetryTimeout();
|
|
21243
|
+
this.clearStopTimeout();
|
|
21244
|
+
this.stopPings();
|
|
21245
|
+
this.userMediaLoading = false;
|
|
21246
|
+
this.connecting = false;
|
|
21247
|
+
this.reconnecting = false;
|
|
21248
|
+
this.reconnectAttempts = 0;
|
|
21249
|
+
if (this.userMedia) this.userMedia.unloadRemainingEventListeners();
|
|
21250
|
+
this.unloaded = true;
|
|
21251
|
+
this.discardSocket();
|
|
21252
|
+
this.built = this.connecting = this.connected = false;
|
|
21042
21253
|
}
|
|
21043
21254
|
correctDimensions() {
|
|
21044
|
-
|
|
21045
|
-
const widthDimension = dimensions_useFullWidth(this.options.video.mobileBreakPoint);
|
|
21046
|
-
if (this.options.video.stretch || widthDimension) {
|
|
21047
|
-
this.recorderElement.style.width = "100%";
|
|
21048
|
-
this.recorderElement.style.removeProperty("height");
|
|
21049
|
-
} else {
|
|
21050
|
-
if (this.options.video.width) {
|
|
21051
|
-
const recorderWidth = this.getRecorderWidth(true);
|
|
21052
|
-
if (recorderWidth?.value) this.recorderElement.width = recorderWidth.value;
|
|
21053
|
-
else this.recorderElement.style.removeProperty("width");
|
|
21054
|
-
}
|
|
21055
|
-
if (this.options.video.height) {
|
|
21056
|
-
const recorderHeight = this.getRecorderHeight(true);
|
|
21057
|
-
if (recorderHeight?.value) this.recorderElement.height = recorderHeight.value;
|
|
21058
|
-
else this.recorderElement.style.removeProperty("height");
|
|
21059
|
-
}
|
|
21060
|
-
}
|
|
21061
|
-
}
|
|
21062
|
-
switchFacingMode() {
|
|
21063
|
-
if (this.cameraRecovery || !util_getBrowser(this.options).isMobile()) return;
|
|
21064
|
-
if ("user" === this.facingMode) this.facingMode = "environment";
|
|
21065
|
-
else if ("environment" === this.facingMode) this.facingMode = "user";
|
|
21066
|
-
else this.options.logger.warn(`Recorder: unsupported facing mode ${string_pretty(this.facingMode)}`);
|
|
21067
|
-
this.loadGenuineUserMedia({
|
|
21068
|
-
switchingFacingMode: this.facingMode
|
|
21069
|
-
});
|
|
21255
|
+
this.dimensions.correctDimensions();
|
|
21070
21256
|
}
|
|
21071
21257
|
initEvents() {
|
|
21072
21258
|
this.options.logger.debug("Recorder: initEvents()");
|
|
@@ -21138,100 +21324,20 @@
|
|
|
21138
21324
|
}
|
|
21139
21325
|
this.built = true;
|
|
21140
21326
|
}
|
|
21141
|
-
isPaused() {
|
|
21142
|
-
return this.userMedia?.isPaused() && !this.loop.isRunning();
|
|
21143
|
-
}
|
|
21144
|
-
isRecording() {
|
|
21145
|
-
return Boolean(this.loop?.isRunning() && !this.isPaused() && !this.isNotifying() && this.stream && !this.stream.destroyed);
|
|
21146
|
-
}
|
|
21147
|
-
hide() {
|
|
21148
|
-
if (!this.isHidden()) {
|
|
21149
|
-
if (this.recorderElement) html_hideElement(this.recorderElement);
|
|
21150
|
-
this.clearUserMediaTimeout();
|
|
21151
|
-
this.clearRetryTimeout();
|
|
21152
|
-
}
|
|
21153
|
-
}
|
|
21154
|
-
isUnloaded() {
|
|
21155
|
-
return this.unloaded;
|
|
21156
|
-
}
|
|
21157
21327
|
getRecorderWidth(responsive) {
|
|
21158
|
-
|
|
21159
|
-
else if (responsive && this.options.video.width) return this.limitWidth(this.options.video.width);
|
|
21160
|
-
const dimension = {
|
|
21161
|
-
unit: "px",
|
|
21162
|
-
value: this.options.video.width
|
|
21163
|
-
};
|
|
21164
|
-
return dimension;
|
|
21165
|
-
}
|
|
21166
|
-
getRecordingDimensions() {
|
|
21167
|
-
return this.recordingDimensions && {
|
|
21168
|
-
...this.recordingDimensions
|
|
21169
|
-
};
|
|
21328
|
+
return this.dimensions.getRecorderWidth(responsive);
|
|
21170
21329
|
}
|
|
21171
21330
|
getRecorderHeight(responsive, useBoundingClientRect) {
|
|
21172
|
-
|
|
21173
|
-
if (this.recorderElement && useBoundingClientRect) {
|
|
21174
|
-
const height = this.recorderElement.getBoundingClientRect().height;
|
|
21175
|
-
recorderHeight = {
|
|
21176
|
-
unit: "px",
|
|
21177
|
-
value: height
|
|
21178
|
-
};
|
|
21179
|
-
} else if (this.userMedia) {
|
|
21180
|
-
let height = this.userMedia.getRawHeight(responsive);
|
|
21181
|
-
if (void 0 !== height && this.options.video.width) {
|
|
21182
|
-
const ratio = this.getRatio();
|
|
21183
|
-
if (void 0 !== ratio) {
|
|
21184
|
-
const idealHeight = this.options.video.width * ratio;
|
|
21185
|
-
height = Math.min(idealHeight, height);
|
|
21186
|
-
}
|
|
21187
|
-
}
|
|
21188
|
-
recorderHeight = {
|
|
21189
|
-
unit: "px",
|
|
21190
|
-
value: height
|
|
21191
|
-
};
|
|
21192
|
-
} else if (responsive && this.options.video.height) recorderHeight = this.calculateHeight(responsive);
|
|
21193
|
-
else if (this.options.video.height) {
|
|
21194
|
-
const height = this.options.video.height;
|
|
21195
|
-
recorderHeight = {
|
|
21196
|
-
unit: "px",
|
|
21197
|
-
value: height
|
|
21198
|
-
};
|
|
21199
|
-
}
|
|
21200
|
-
return recorderHeight;
|
|
21331
|
+
return this.dimensions.getRecorderHeight(responsive, useBoundingClientRect);
|
|
21201
21332
|
}
|
|
21202
21333
|
getRatio() {
|
|
21203
|
-
|
|
21204
|
-
if (this.userMedia) {
|
|
21205
|
-
const userMediaVideoWidth = this.userMedia.getVideoWidth();
|
|
21206
|
-
const userMediaVideoHeight = this.userMedia.getVideoHeight();
|
|
21207
|
-
if (!userMediaVideoWidth || userMediaVideoWidth < 1) ratio = this.visuals.getRatio();
|
|
21208
|
-
else if (userMediaVideoHeight) ratio = userMediaVideoHeight / userMediaVideoWidth;
|
|
21209
|
-
} else ratio = dimensions_getRatio(this.options);
|
|
21210
|
-
return ratio;
|
|
21334
|
+
return this.dimensions.getRatio();
|
|
21211
21335
|
}
|
|
21212
21336
|
calculateWidth(responsive) {
|
|
21213
|
-
|
|
21214
|
-
if (this.userMedia) videoHeight = this.userMedia.getVideoHeight();
|
|
21215
|
-
else if (this.recorderElement) videoHeight = this.recorderElement.videoHeight || this.recorderElement.height;
|
|
21216
|
-
const ratio = this.getRatio();
|
|
21217
|
-
return dimensions_calculateWidth(responsive, this.options, videoHeight, ratio);
|
|
21337
|
+
return this.dimensions.calculateWidth(responsive);
|
|
21218
21338
|
}
|
|
21219
21339
|
calculateHeight(responsive) {
|
|
21220
|
-
|
|
21221
|
-
if (this.userMedia) {
|
|
21222
|
-
const videoHeight = this.userMedia.getVideoHeight();
|
|
21223
|
-
videoDimension = {
|
|
21224
|
-
value: videoHeight,
|
|
21225
|
-
unit: "px"
|
|
21226
|
-
};
|
|
21227
|
-
} else if (this.recorderElement) {
|
|
21228
|
-
const videoHeight = this.recorderElement.videoHeight || this.recorderElement.height;
|
|
21229
|
-
videoDimension = {
|
|
21230
|
-
value: videoHeight,
|
|
21231
|
-
unit: "px"
|
|
21232
|
-
};
|
|
21233
|
-
} else videoDimension = dimensions_calculateHeight(responsive, void 0, this.options, this.getRatio(), this.recorderElement);
|
|
21234
|
-
return videoDimension;
|
|
21340
|
+
return this.dimensions.calculateHeight(responsive);
|
|
21235
21341
|
}
|
|
21236
21342
|
getRawVisualUserMedia() {
|
|
21237
21343
|
return this.recorderElement;
|
|
@@ -21248,9 +21354,6 @@
|
|
|
21248
21354
|
limitHeight(height) {
|
|
21249
21355
|
return this.visuals.limitHeight(height);
|
|
21250
21356
|
}
|
|
21251
|
-
isUserMediaLoaded() {
|
|
21252
|
-
return this.userMediaLoaded;
|
|
21253
|
-
}
|
|
21254
21357
|
}
|
|
21255
21358
|
const visuals_recorder = Recorder;
|
|
21256
21359
|
class Replay extends util_Despot {
|
|
@@ -22162,7 +22265,8 @@
|
|
|
22162
22265
|
}
|
|
22163
22266
|
else runValidation = false;
|
|
22164
22267
|
if (runValidation) {
|
|
22165
|
-
const
|
|
22268
|
+
const target = event?.target;
|
|
22269
|
+
const targetName = target instanceof Element ? target.getAttribute("name") : void 0;
|
|
22166
22270
|
if (targetName) this.emit("VALIDATING", {
|
|
22167
22271
|
targetName
|
|
22168
22272
|
});
|
|
@@ -22197,27 +22301,10 @@
|
|
|
22197
22301
|
}
|
|
22198
22302
|
}
|
|
22199
22303
|
if (valid) {
|
|
22200
|
-
|
|
22201
|
-
|
|
22202
|
-
|
|
22203
|
-
const bccIsConfigured = "bcc" in recipients;
|
|
22204
|
-
const hasTo = recipients.to && recipients.to.length > 0;
|
|
22205
|
-
const hasCc = recipients.cc && recipients.cc.length > 0;
|
|
22206
|
-
const hasBcc = recipients.bcc && recipients.bcc.length > 0;
|
|
22207
|
-
if (toIsConfigured) {
|
|
22208
|
-
if (!hasTo) if (ccIsConfigured && bccIsConfigured) {
|
|
22209
|
-
if (!hasCc && !hasBcc) valid = false;
|
|
22210
|
-
} else if (ccIsConfigured) {
|
|
22211
|
-
if (!hasCc) valid = false;
|
|
22212
|
-
} else if (bccIsConfigured) {
|
|
22213
|
-
if (!hasBcc) valid = false;
|
|
22214
|
-
} else valid = false;
|
|
22215
|
-
} else if (ccIsConfigured) {
|
|
22216
|
-
if (!hasCc) {
|
|
22217
|
-
if (bccIsConfigured && !hasBcc) valid = false;
|
|
22218
|
-
}
|
|
22304
|
+
if (!email_isRecipientSelectionValid(this.form.getRecipients())) {
|
|
22305
|
+
valid = false;
|
|
22306
|
+
whyInvalid = "At least one recipient is required";
|
|
22219
22307
|
}
|
|
22220
|
-
if (!valid) whyInvalid = "At least one recipient is required";
|
|
22221
22308
|
}
|
|
22222
22309
|
} else {
|
|
22223
22310
|
valid = visualsValid;
|