@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.
Files changed (3) hide show
  1. package/dist/index.d.ts +2296 -2258
  2. package/dist/index.js +198 -24
  3. 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}${Date.now()}-${Math.random().toString(36).substring(2, 2 + ID_RANDOM_LENGTH)}`;
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.4",
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
- if (retryCount >= MAX_RETRIES2) {
7218
- this.callbacks.onUploadError?.(upload.id, new Error(`Upload failed after ${MAX_RETRIES2} attempts: ${errorMessage}`));
7219
- } else {
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
- return this.uploadVideoFile(blob, options);
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
- if (options.onProgress) {
7269
- const onProgress = options.onProgress;
7270
- xhr.upload.addEventListener("progress", (event) => {
7271
- if (event.lengthComputable) {
7272
- const progress = event.loaded / event.total;
7273
- onProgress(progress);
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, resolve, reject);
7356
+ this.parseSuccessResponse(xhr, resolveRequest, rejectRequest);
7280
7357
  return;
7281
7358
  }
7282
- this.parseErrorResponse(xhr, reject);
7359
+ this.parseErrorResponse(xhr, rejectRequest);
7283
7360
  });
7284
7361
  xhr.addEventListener("error", () => {
7285
- reject(new Error("Network error during upload"));
7362
+ rejectRequest(new Error("Network error during upload"));
7286
7363
  });
7287
7364
  xhr.addEventListener("abort", () => {
7288
- reject(new Error("Upload was aborted"));
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.4",
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",