@vidtreo/recorder 1.7.4 → 1.7.6
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/dist/index.d.ts +2296 -2258
- package/dist/index.js +198 -24
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3075,7 +3075,6 @@ var DEFAULT_RETENTION_HOURS = 24;
|
|
|
3075
3075
|
var MAX_RETRIES = 10;
|
|
3076
3076
|
var MILLISECONDS_PER_HOUR = 60 * 60 * 1000;
|
|
3077
3077
|
var ID_PREFIX = "upload-";
|
|
3078
|
-
var ID_RANDOM_LENGTH = 9;
|
|
3079
3078
|
var VERSION_ERROR_NAME = "VersionError";
|
|
3080
3079
|
var ERROR_SCHEMA_MISSING_STORE = "Database schema is missing required object store: pending-uploads";
|
|
3081
3080
|
var ERROR_SCHEMA_MISSING_STATUS_INDEX = "Database schema is missing required index: status";
|
|
@@ -3371,7 +3370,7 @@ class VideoStorageService {
|
|
|
3371
3370
|
});
|
|
3372
3371
|
}
|
|
3373
3372
|
generateUploadId() {
|
|
3374
|
-
return `${ID_PREFIX}${
|
|
3373
|
+
return `${ID_PREFIX}${crypto.randomUUID()}`;
|
|
3375
3374
|
}
|
|
3376
3375
|
executeTransaction(mode, operation) {
|
|
3377
3376
|
if (!this.db) {
|
|
@@ -6312,7 +6311,7 @@ function resolveDeviceError(input) {
|
|
|
6312
6311
|
// package.json
|
|
6313
6312
|
var package_default = {
|
|
6314
6313
|
name: "@vidtreo/recorder",
|
|
6315
|
-
version: "1.7.
|
|
6314
|
+
version: "1.7.6",
|
|
6316
6315
|
type: "module",
|
|
6317
6316
|
description: "Vidtreo SDK for browser-based video recording and transcoding. Features include camera/screen recording, real-time MP4 transcoding, audio level analysis, mute/pause controls, source switching, device selection, and automatic backend uploads. Similar to Ziggeo and Addpipe, Vidtreo provides enterprise-grade video processing capabilities for web applications.",
|
|
6318
6317
|
main: "./dist/index.js",
|
|
@@ -6452,6 +6451,8 @@ var TELEMETRY_EVENT_CATEGORY_MAP = {
|
|
|
6452
6451
|
"recording.stop.failed": "error",
|
|
6453
6452
|
"recording.route.selected": "lifecycle",
|
|
6454
6453
|
"upload.started": "performance",
|
|
6454
|
+
"upload.attempt.failed": "error",
|
|
6455
|
+
"upload.retry.scheduled": "performance",
|
|
6455
6456
|
"upload.succeeded": "performance",
|
|
6456
6457
|
"upload.failed": "error",
|
|
6457
6458
|
"upload.route.selected": "lifecycle",
|
|
@@ -7188,6 +7189,12 @@ class UploadQueueManager {
|
|
|
7188
7189
|
return Object.entries(expected).every(([key, value]) => actual[key] === value);
|
|
7189
7190
|
}
|
|
7190
7191
|
async processUpload(upload, shouldThrowOnError = false) {
|
|
7192
|
+
const attempt = upload.retryCount + 1;
|
|
7193
|
+
let progressDetails = {
|
|
7194
|
+
progress: 0,
|
|
7195
|
+
loadedBytes: 0,
|
|
7196
|
+
totalBytes: 0
|
|
7197
|
+
};
|
|
7191
7198
|
try {
|
|
7192
7199
|
await this.storageService.updateUploadStatus(upload.id, {
|
|
7193
7200
|
status: "uploading"
|
|
@@ -7196,29 +7203,58 @@ class UploadQueueManager {
|
|
|
7196
7203
|
apiKey: upload.apiKey,
|
|
7197
7204
|
backendUrl: upload.backendUrl,
|
|
7198
7205
|
filename: upload.filename,
|
|
7206
|
+
idempotencyKey: upload.id,
|
|
7199
7207
|
mimeType: upload.mimeType,
|
|
7200
7208
|
metadata: upload.metadata,
|
|
7201
7209
|
userMetadata: upload.userMetadata,
|
|
7202
7210
|
onProgress: (progress) => {
|
|
7203
7211
|
this.callbacks.onUploadProgress?.(upload.id, progress);
|
|
7212
|
+
},
|
|
7213
|
+
onProgressDetails: (details) => {
|
|
7214
|
+
progressDetails = details;
|
|
7215
|
+
this.callbacks.onUploadProgressDetails?.(upload.id, details);
|
|
7204
7216
|
}
|
|
7205
7217
|
});
|
|
7218
|
+
this.callbacks.onUploadAttemptSucceeded?.(upload.id, {
|
|
7219
|
+
attempt,
|
|
7220
|
+
maxAttempts: MAX_RETRIES2,
|
|
7221
|
+
loadedBytes: progressDetails.loadedBytes,
|
|
7222
|
+
totalBytes: progressDetails.totalBytes
|
|
7223
|
+
});
|
|
7206
7224
|
await this.storageService.deleteUpload(upload.id);
|
|
7207
7225
|
this.callbacks.onUploadComplete?.(upload.id, result);
|
|
7208
7226
|
return result;
|
|
7209
7227
|
} catch (error) {
|
|
7210
7228
|
const errorMessage = extractErrorMessage(error);
|
|
7211
7229
|
const retryCount = upload.retryCount + 1;
|
|
7230
|
+
const willRetry = retryCount < MAX_RETRIES2;
|
|
7231
|
+
const retryDelayMs = willRetry ? this.calculateRetryDelay(retryCount) : null;
|
|
7212
7232
|
await this.storageService.updateUploadStatus(upload.id, {
|
|
7213
7233
|
status: "failed",
|
|
7214
7234
|
retryCount,
|
|
7215
7235
|
lastError: errorMessage
|
|
7216
7236
|
});
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7237
|
+
this.callbacks.onUploadAttemptFailed?.(upload.id, {
|
|
7238
|
+
attempt,
|
|
7239
|
+
maxAttempts: MAX_RETRIES2,
|
|
7240
|
+
loadedBytes: progressDetails.loadedBytes,
|
|
7241
|
+
totalBytes: progressDetails.totalBytes,
|
|
7242
|
+
reason: errorMessage,
|
|
7243
|
+
retryDelayMs,
|
|
7244
|
+
willRetry
|
|
7245
|
+
});
|
|
7246
|
+
if (willRetry) {
|
|
7220
7247
|
const delay2 = this.calculateRetryDelay(retryCount);
|
|
7248
|
+
this.callbacks.onUploadProgress?.(upload.id, 0);
|
|
7249
|
+
this.callbacks.onUploadRetry?.(upload.id, {
|
|
7250
|
+
attempt: attempt + 1,
|
|
7251
|
+
maxAttempts: MAX_RETRIES2,
|
|
7252
|
+
reason: errorMessage,
|
|
7253
|
+
retryDelayMs: delay2
|
|
7254
|
+
});
|
|
7221
7255
|
this.scheduleRetry(delay2);
|
|
7256
|
+
} else {
|
|
7257
|
+
this.callbacks.onUploadError?.(upload.id, new Error(`Upload failed after ${MAX_RETRIES2} attempts: ${errorMessage}`));
|
|
7222
7258
|
}
|
|
7223
7259
|
if (shouldThrowOnError) {
|
|
7224
7260
|
throw new Error(errorMessage);
|
|
@@ -7250,6 +7286,9 @@ class UploadQueueManager {
|
|
|
7250
7286
|
// src/core/upload/upload-service.ts
|
|
7251
7287
|
var API_UPLOAD_PATH = "/api/v1/videos/upload";
|
|
7252
7288
|
var MIME_TYPE_PARAMETER_SEPARATOR = ";";
|
|
7289
|
+
var MAX_TRANSFER_PROGRESS = 0.99;
|
|
7290
|
+
var UPLOAD_INACTIVITY_TIMEOUT_MS = 45000;
|
|
7291
|
+
var UPLOAD_INACTIVITY_ERROR = "Upload stalled after 45 seconds without progress";
|
|
7253
7292
|
|
|
7254
7293
|
class VideoUploadService {
|
|
7255
7294
|
uploadVideo(blob, options) {
|
|
@@ -7259,37 +7298,76 @@ class VideoUploadService {
|
|
|
7259
7298
|
if (!blob.type || blob.type.trim() === "") {
|
|
7260
7299
|
throw new Error("Blob type is required");
|
|
7261
7300
|
}
|
|
7262
|
-
|
|
7301
|
+
const idempotencyKey = this.resolveIdempotencyKey(options.idempotencyKey);
|
|
7302
|
+
return this.uploadVideoFile(blob, options, idempotencyKey);
|
|
7263
7303
|
}
|
|
7264
|
-
uploadVideoFile(blob, options) {
|
|
7304
|
+
uploadVideoFile(blob, options, idempotencyKey) {
|
|
7265
7305
|
return new Promise((resolve, reject) => {
|
|
7266
7306
|
const xhr = new XMLHttpRequest;
|
|
7267
7307
|
const uploadBlob = this.createUploadBlob(blob, options.mimeType);
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
}
|
|
7276
|
-
|
|
7308
|
+
let inactivityTimeoutId = null;
|
|
7309
|
+
let isSettled = false;
|
|
7310
|
+
let loadedBytes = 0;
|
|
7311
|
+
let totalBytes = 0;
|
|
7312
|
+
const clearInactivityTimeout = () => {
|
|
7313
|
+
if (inactivityTimeoutId === null) {
|
|
7314
|
+
return;
|
|
7315
|
+
}
|
|
7316
|
+
window.clearTimeout(inactivityTimeoutId);
|
|
7317
|
+
inactivityTimeoutId = null;
|
|
7318
|
+
};
|
|
7319
|
+
const rejectRequest = (reason, shouldAbort = false) => {
|
|
7320
|
+
if (isSettled) {
|
|
7321
|
+
return;
|
|
7322
|
+
}
|
|
7323
|
+
isSettled = true;
|
|
7324
|
+
clearInactivityTimeout();
|
|
7325
|
+
if (shouldAbort) {
|
|
7326
|
+
xhr.abort();
|
|
7327
|
+
}
|
|
7328
|
+
reject(reason);
|
|
7329
|
+
};
|
|
7330
|
+
const resolveRequest = (result) => {
|
|
7331
|
+
if (isSettled) {
|
|
7332
|
+
return;
|
|
7333
|
+
}
|
|
7334
|
+
isSettled = true;
|
|
7335
|
+
clearInactivityTimeout();
|
|
7336
|
+
this.reportCompletion(options, loadedBytes, totalBytes);
|
|
7337
|
+
resolve(result);
|
|
7338
|
+
};
|
|
7339
|
+
const restartInactivityTimeout = () => {
|
|
7340
|
+
clearInactivityTimeout();
|
|
7341
|
+
inactivityTimeoutId = window.setTimeout(() => {
|
|
7342
|
+
rejectRequest(new Error(UPLOAD_INACTIVITY_ERROR), true);
|
|
7343
|
+
}, UPLOAD_INACTIVITY_TIMEOUT_MS);
|
|
7344
|
+
};
|
|
7345
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
7346
|
+
this.reportProgress(event, options);
|
|
7347
|
+
if (event.loaded <= loadedBytes) {
|
|
7348
|
+
return;
|
|
7349
|
+
}
|
|
7350
|
+
loadedBytes = event.loaded;
|
|
7351
|
+
totalBytes = event.total;
|
|
7352
|
+
restartInactivityTimeout();
|
|
7353
|
+
});
|
|
7277
7354
|
xhr.addEventListener("load", () => {
|
|
7278
7355
|
if (xhr.status >= 200 && xhr.status <= 299) {
|
|
7279
|
-
this.parseSuccessResponse(xhr,
|
|
7356
|
+
this.parseSuccessResponse(xhr, resolveRequest, rejectRequest);
|
|
7280
7357
|
return;
|
|
7281
7358
|
}
|
|
7282
|
-
this.parseErrorResponse(xhr,
|
|
7359
|
+
this.parseErrorResponse(xhr, rejectRequest);
|
|
7283
7360
|
});
|
|
7284
7361
|
xhr.addEventListener("error", () => {
|
|
7285
|
-
|
|
7362
|
+
rejectRequest(new Error("Network error during upload"));
|
|
7286
7363
|
});
|
|
7287
7364
|
xhr.addEventListener("abort", () => {
|
|
7288
|
-
|
|
7365
|
+
rejectRequest(new Error("Upload was aborted"));
|
|
7289
7366
|
});
|
|
7290
7367
|
const url = `${options.backendUrl}${API_UPLOAD_PATH}`;
|
|
7291
7368
|
xhr.open("POST", url);
|
|
7292
7369
|
xhr.setRequestHeader("Authorization", `Bearer ${options.apiKey}`);
|
|
7370
|
+
xhr.setRequestHeader("Idempotency-Key", idempotencyKey);
|
|
7293
7371
|
const formData = new FormData;
|
|
7294
7372
|
formData.append("file", uploadBlob, options.filename);
|
|
7295
7373
|
const metadata = {
|
|
@@ -7301,6 +7379,33 @@ class VideoUploadService {
|
|
|
7301
7379
|
}
|
|
7302
7380
|
formData.append("metadata", JSON.stringify(metadata));
|
|
7303
7381
|
xhr.send(formData);
|
|
7382
|
+
restartInactivityTimeout();
|
|
7383
|
+
});
|
|
7384
|
+
}
|
|
7385
|
+
resolveIdempotencyKey(idempotencyKey) {
|
|
7386
|
+
if (idempotencyKey) {
|
|
7387
|
+
return idempotencyKey;
|
|
7388
|
+
}
|
|
7389
|
+
return crypto.randomUUID();
|
|
7390
|
+
}
|
|
7391
|
+
reportProgress(event, options) {
|
|
7392
|
+
if (!event.lengthComputable) {
|
|
7393
|
+
return;
|
|
7394
|
+
}
|
|
7395
|
+
const progress = Math.min(event.loaded / event.total, MAX_TRANSFER_PROGRESS);
|
|
7396
|
+
options.onProgress?.(progress);
|
|
7397
|
+
options.onProgressDetails?.({
|
|
7398
|
+
progress,
|
|
7399
|
+
loadedBytes: event.loaded,
|
|
7400
|
+
totalBytes: event.total
|
|
7401
|
+
});
|
|
7402
|
+
}
|
|
7403
|
+
reportCompletion(options, loadedBytes, totalBytes) {
|
|
7404
|
+
options.onProgress?.(1);
|
|
7405
|
+
options.onProgressDetails?.({
|
|
7406
|
+
progress: 1,
|
|
7407
|
+
loadedBytes,
|
|
7408
|
+
totalBytes
|
|
7304
7409
|
});
|
|
7305
7410
|
}
|
|
7306
7411
|
createUploadBlob(blob, mimeType) {
|
|
@@ -23210,6 +23315,8 @@ class RecorderController {
|
|
|
23210
23315
|
callbacks;
|
|
23211
23316
|
telemetryManager;
|
|
23212
23317
|
uploadMetadataManager;
|
|
23318
|
+
uploadAttemptFailureById = new Map;
|
|
23319
|
+
uploadAttemptSuccessById = new Map;
|
|
23213
23320
|
uploadQueueManager = null;
|
|
23214
23321
|
isInitialized = false;
|
|
23215
23322
|
isDemo = false;
|
|
@@ -23551,6 +23658,11 @@ class RecorderController {
|
|
|
23551
23658
|
}, error);
|
|
23552
23659
|
throw error;
|
|
23553
23660
|
}
|
|
23661
|
+
let progressDetails = {
|
|
23662
|
+
progress: 0,
|
|
23663
|
+
loadedBytes: 0,
|
|
23664
|
+
totalBytes: 0
|
|
23665
|
+
};
|
|
23554
23666
|
try {
|
|
23555
23667
|
const result = await this.uploadService.uploadVideo(options.blob, {
|
|
23556
23668
|
apiKey: options.apiKey,
|
|
@@ -23559,18 +23671,37 @@ class RecorderController {
|
|
|
23559
23671
|
mimeType: options.mimeType,
|
|
23560
23672
|
metadata: undefined,
|
|
23561
23673
|
userMetadata: options.userMetadata,
|
|
23562
|
-
onProgress: this.uploadCallbacks.onProgress
|
|
23674
|
+
onProgress: this.uploadCallbacks.onProgress,
|
|
23675
|
+
onProgressDetails: (details) => {
|
|
23676
|
+
progressDetails = details;
|
|
23677
|
+
}
|
|
23563
23678
|
});
|
|
23564
23679
|
this.uploadCallbacks.onSuccess(result);
|
|
23565
23680
|
this.telemetryManager.sendEvent("upload.succeeded", {
|
|
23566
23681
|
filename: options.filename,
|
|
23567
23682
|
duration: options.duration,
|
|
23568
23683
|
sourceType: options.sourceType,
|
|
23569
|
-
recordingId: result.id
|
|
23684
|
+
recordingId: result.id,
|
|
23685
|
+
attempt: 1,
|
|
23686
|
+
maxAttempts: 1,
|
|
23687
|
+
loadedBytes: progressDetails.loadedBytes,
|
|
23688
|
+
totalBytes: progressDetails.totalBytes
|
|
23570
23689
|
});
|
|
23571
23690
|
} catch (error) {
|
|
23572
23691
|
const uploadError = error instanceof Error ? error : new Error(extractErrorMessage(error));
|
|
23573
23692
|
this.uploadCallbacks.onError(uploadError);
|
|
23693
|
+
this.telemetryManager.sendEvent("upload.attempt.failed", {
|
|
23694
|
+
filename: options.filename,
|
|
23695
|
+
duration: options.duration,
|
|
23696
|
+
sourceType: options.sourceType,
|
|
23697
|
+
attempt: 1,
|
|
23698
|
+
maxAttempts: 1,
|
|
23699
|
+
loadedBytes: progressDetails.loadedBytes,
|
|
23700
|
+
totalBytes: progressDetails.totalBytes,
|
|
23701
|
+
reason: uploadError.message,
|
|
23702
|
+
willRetry: false,
|
|
23703
|
+
retryDelayMs: null
|
|
23704
|
+
}, uploadError);
|
|
23574
23705
|
this.telemetryManager.sendEvent("upload.failed", {
|
|
23575
23706
|
filename: options.filename,
|
|
23576
23707
|
duration: options.duration,
|
|
@@ -23819,19 +23950,60 @@ class RecorderController {
|
|
|
23819
23950
|
onUploadProgress: (_id, progress) => {
|
|
23820
23951
|
this.uploadCallbacks.onProgress(progress);
|
|
23821
23952
|
},
|
|
23953
|
+
onUploadAttemptFailed: (id, details) => {
|
|
23954
|
+
this.uploadAttemptFailureById.set(id, details);
|
|
23955
|
+
const metadata = this.uploadMetadataManager.getMetadata(id);
|
|
23956
|
+
if (!metadata) {
|
|
23957
|
+
return;
|
|
23958
|
+
}
|
|
23959
|
+
const error = new Error(details.reason);
|
|
23960
|
+
this.telemetryManager.sendEvent("upload.attempt.failed", {
|
|
23961
|
+
filename: metadata.filename,
|
|
23962
|
+
duration: metadata.duration,
|
|
23963
|
+
sourceType: metadata.sourceType,
|
|
23964
|
+
...details
|
|
23965
|
+
}, error);
|
|
23966
|
+
},
|
|
23967
|
+
onUploadAttemptSucceeded: (id, details) => {
|
|
23968
|
+
this.uploadAttemptSuccessById.set(id, details);
|
|
23969
|
+
},
|
|
23970
|
+
onUploadRetry: (id, details) => {
|
|
23971
|
+
this.uploadCallbacks.onRetry?.(details);
|
|
23972
|
+
const metadata = this.uploadMetadataManager.getMetadata(id);
|
|
23973
|
+
const failureDetails = this.uploadAttemptFailureById.get(id);
|
|
23974
|
+
if (!(metadata && failureDetails)) {
|
|
23975
|
+
return;
|
|
23976
|
+
}
|
|
23977
|
+
this.telemetryManager.sendEvent("upload.retry.scheduled", {
|
|
23978
|
+
filename: metadata.filename,
|
|
23979
|
+
duration: metadata.duration,
|
|
23980
|
+
sourceType: metadata.sourceType,
|
|
23981
|
+
attempt: details.attempt,
|
|
23982
|
+
maxAttempts: details.maxAttempts,
|
|
23983
|
+
previousAttempt: failureDetails.attempt,
|
|
23984
|
+
loadedBytes: failureDetails.loadedBytes,
|
|
23985
|
+
totalBytes: failureDetails.totalBytes,
|
|
23986
|
+
reason: details.reason,
|
|
23987
|
+
retryDelayMs: details.retryDelayMs
|
|
23988
|
+
});
|
|
23989
|
+
},
|
|
23822
23990
|
onUploadComplete: (id, result) => {
|
|
23823
23991
|
this.isUploadInProgress = false;
|
|
23824
23992
|
this.uploadCallbacks.onSuccess(result);
|
|
23825
23993
|
const metadata = this.uploadMetadataManager.getMetadata(id);
|
|
23994
|
+
const successDetails = this.uploadAttemptSuccessById.get(id);
|
|
23826
23995
|
if (metadata) {
|
|
23827
23996
|
this.telemetryManager.sendEvent("upload.succeeded", {
|
|
23828
23997
|
filename: metadata.filename,
|
|
23829
23998
|
duration: metadata.duration,
|
|
23830
23999
|
sourceType: metadata.sourceType,
|
|
23831
|
-
recordingId: result.id
|
|
24000
|
+
recordingId: result.id,
|
|
24001
|
+
...successDetails && successDetails
|
|
23832
24002
|
});
|
|
23833
24003
|
this.uploadMetadataManager.clearMetadata(id);
|
|
23834
24004
|
}
|
|
24005
|
+
this.uploadAttemptFailureById.delete(id);
|
|
24006
|
+
this.uploadAttemptSuccessById.delete(id);
|
|
23835
24007
|
},
|
|
23836
24008
|
onUploadError: (id, error) => {
|
|
23837
24009
|
this.isUploadInProgress = false;
|
|
@@ -23845,6 +24017,8 @@ class RecorderController {
|
|
|
23845
24017
|
}, error);
|
|
23846
24018
|
this.uploadMetadataManager.clearMetadata(id);
|
|
23847
24019
|
}
|
|
24020
|
+
this.uploadAttemptFailureById.delete(id);
|
|
24021
|
+
this.uploadAttemptSuccessById.delete(id);
|
|
23848
24022
|
}
|
|
23849
24023
|
});
|
|
23850
24024
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vidtreo/recorder",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vidtreo SDK for browser-based video recording and transcoding. Features include camera/screen recording, real-time MP4 transcoding, audio level analysis, mute/pause controls, source switching, device selection, and automatic backend uploads. Similar to Ziggeo and Addpipe, Vidtreo provides enterprise-grade video processing capabilities for web applications.",
|
|
6
6
|
"main": "./dist/index.js",
|