@vidtreo/recorder 1.7.4 → 1.7.5

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 +2293 -2258
  2. package/dist/index.js +174 -20
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6312,7 +6312,7 @@ function resolveDeviceError(input) {
6312
6312
  // package.json
6313
6313
  var package_default = {
6314
6314
  name: "@vidtreo/recorder",
6315
- version: "1.7.4",
6315
+ version: "1.7.5",
6316
6316
  type: "module",
6317
6317
  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
6318
  main: "./dist/index.js",
@@ -6452,6 +6452,8 @@ var TELEMETRY_EVENT_CATEGORY_MAP = {
6452
6452
  "recording.stop.failed": "error",
6453
6453
  "recording.route.selected": "lifecycle",
6454
6454
  "upload.started": "performance",
6455
+ "upload.attempt.failed": "error",
6456
+ "upload.retry.scheduled": "performance",
6455
6457
  "upload.succeeded": "performance",
6456
6458
  "upload.failed": "error",
6457
6459
  "upload.route.selected": "lifecycle",
@@ -7188,6 +7190,12 @@ class UploadQueueManager {
7188
7190
  return Object.entries(expected).every(([key, value]) => actual[key] === value);
7189
7191
  }
7190
7192
  async processUpload(upload, shouldThrowOnError = false) {
7193
+ const attempt = upload.retryCount + 1;
7194
+ let progressDetails = {
7195
+ progress: 0,
7196
+ loadedBytes: 0,
7197
+ totalBytes: 0
7198
+ };
7191
7199
  try {
7192
7200
  await this.storageService.updateUploadStatus(upload.id, {
7193
7201
  status: "uploading"
@@ -7201,24 +7209,52 @@ class UploadQueueManager {
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,8 @@ 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 UPLOAD_INACTIVITY_TIMEOUT_MS = 45000;
7290
+ var UPLOAD_INACTIVITY_ERROR = "Upload stalled after 45 seconds without progress";
7253
7291
 
7254
7292
  class VideoUploadService {
7255
7293
  uploadVideo(blob, options) {
@@ -7265,27 +7303,61 @@ class VideoUploadService {
7265
7303
  return new Promise((resolve, reject) => {
7266
7304
  const xhr = new XMLHttpRequest;
7267
7305
  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
- }
7306
+ let inactivityTimeoutId = null;
7307
+ let isSettled = false;
7308
+ let loadedBytes = 0;
7309
+ const clearInactivityTimeout = () => {
7310
+ if (inactivityTimeoutId === null) {
7311
+ return;
7312
+ }
7313
+ window.clearTimeout(inactivityTimeoutId);
7314
+ inactivityTimeoutId = null;
7315
+ };
7316
+ const rejectRequest = (reason, shouldAbort = false) => {
7317
+ if (isSettled) {
7318
+ return;
7319
+ }
7320
+ isSettled = true;
7321
+ clearInactivityTimeout();
7322
+ if (shouldAbort) {
7323
+ xhr.abort();
7324
+ }
7325
+ reject(reason);
7326
+ };
7327
+ const resolveRequest = (result) => {
7328
+ if (isSettled) {
7329
+ return;
7330
+ }
7331
+ isSettled = true;
7332
+ clearInactivityTimeout();
7333
+ resolve(result);
7334
+ };
7335
+ const restartInactivityTimeout = () => {
7336
+ clearInactivityTimeout();
7337
+ inactivityTimeoutId = window.setTimeout(() => {
7338
+ rejectRequest(new Error(UPLOAD_INACTIVITY_ERROR), true);
7339
+ }, UPLOAD_INACTIVITY_TIMEOUT_MS);
7340
+ };
7341
+ xhr.upload.addEventListener("progress", (event) => {
7342
+ this.reportProgress(event, options);
7343
+ if (event.loaded <= loadedBytes) {
7344
+ return;
7345
+ }
7346
+ loadedBytes = event.loaded;
7347
+ restartInactivityTimeout();
7348
+ });
7277
7349
  xhr.addEventListener("load", () => {
7278
7350
  if (xhr.status >= 200 && xhr.status <= 299) {
7279
- this.parseSuccessResponse(xhr, resolve, reject);
7351
+ this.parseSuccessResponse(xhr, resolveRequest, rejectRequest);
7280
7352
  return;
7281
7353
  }
7282
- this.parseErrorResponse(xhr, reject);
7354
+ this.parseErrorResponse(xhr, rejectRequest);
7283
7355
  });
7284
7356
  xhr.addEventListener("error", () => {
7285
- reject(new Error("Network error during upload"));
7357
+ rejectRequest(new Error("Network error during upload"));
7286
7358
  });
7287
7359
  xhr.addEventListener("abort", () => {
7288
- reject(new Error("Upload was aborted"));
7360
+ rejectRequest(new Error("Upload was aborted"));
7289
7361
  });
7290
7362
  const url = `${options.backendUrl}${API_UPLOAD_PATH}`;
7291
7363
  xhr.open("POST", url);
@@ -7301,6 +7373,19 @@ class VideoUploadService {
7301
7373
  }
7302
7374
  formData.append("metadata", JSON.stringify(metadata));
7303
7375
  xhr.send(formData);
7376
+ restartInactivityTimeout();
7377
+ });
7378
+ }
7379
+ reportProgress(event, options) {
7380
+ if (!event.lengthComputable) {
7381
+ return;
7382
+ }
7383
+ const progress = event.loaded / event.total;
7384
+ options.onProgress?.(progress);
7385
+ options.onProgressDetails?.({
7386
+ progress,
7387
+ loadedBytes: event.loaded,
7388
+ totalBytes: event.total
7304
7389
  });
7305
7390
  }
7306
7391
  createUploadBlob(blob, mimeType) {
@@ -23210,6 +23295,8 @@ class RecorderController {
23210
23295
  callbacks;
23211
23296
  telemetryManager;
23212
23297
  uploadMetadataManager;
23298
+ uploadAttemptFailureById = new Map;
23299
+ uploadAttemptSuccessById = new Map;
23213
23300
  uploadQueueManager = null;
23214
23301
  isInitialized = false;
23215
23302
  isDemo = false;
@@ -23551,6 +23638,11 @@ class RecorderController {
23551
23638
  }, error);
23552
23639
  throw error;
23553
23640
  }
23641
+ let progressDetails = {
23642
+ progress: 0,
23643
+ loadedBytes: 0,
23644
+ totalBytes: 0
23645
+ };
23554
23646
  try {
23555
23647
  const result = await this.uploadService.uploadVideo(options.blob, {
23556
23648
  apiKey: options.apiKey,
@@ -23559,18 +23651,37 @@ class RecorderController {
23559
23651
  mimeType: options.mimeType,
23560
23652
  metadata: undefined,
23561
23653
  userMetadata: options.userMetadata,
23562
- onProgress: this.uploadCallbacks.onProgress
23654
+ onProgress: this.uploadCallbacks.onProgress,
23655
+ onProgressDetails: (details) => {
23656
+ progressDetails = details;
23657
+ }
23563
23658
  });
23564
23659
  this.uploadCallbacks.onSuccess(result);
23565
23660
  this.telemetryManager.sendEvent("upload.succeeded", {
23566
23661
  filename: options.filename,
23567
23662
  duration: options.duration,
23568
23663
  sourceType: options.sourceType,
23569
- recordingId: result.id
23664
+ recordingId: result.id,
23665
+ attempt: 1,
23666
+ maxAttempts: 1,
23667
+ loadedBytes: progressDetails.loadedBytes,
23668
+ totalBytes: progressDetails.totalBytes
23570
23669
  });
23571
23670
  } catch (error) {
23572
23671
  const uploadError = error instanceof Error ? error : new Error(extractErrorMessage(error));
23573
23672
  this.uploadCallbacks.onError(uploadError);
23673
+ this.telemetryManager.sendEvent("upload.attempt.failed", {
23674
+ filename: options.filename,
23675
+ duration: options.duration,
23676
+ sourceType: options.sourceType,
23677
+ attempt: 1,
23678
+ maxAttempts: 1,
23679
+ loadedBytes: progressDetails.loadedBytes,
23680
+ totalBytes: progressDetails.totalBytes,
23681
+ reason: uploadError.message,
23682
+ willRetry: false,
23683
+ retryDelayMs: null
23684
+ }, uploadError);
23574
23685
  this.telemetryManager.sendEvent("upload.failed", {
23575
23686
  filename: options.filename,
23576
23687
  duration: options.duration,
@@ -23819,19 +23930,60 @@ class RecorderController {
23819
23930
  onUploadProgress: (_id, progress) => {
23820
23931
  this.uploadCallbacks.onProgress(progress);
23821
23932
  },
23933
+ onUploadAttemptFailed: (id, details) => {
23934
+ this.uploadAttemptFailureById.set(id, details);
23935
+ const metadata = this.uploadMetadataManager.getMetadata(id);
23936
+ if (!metadata) {
23937
+ return;
23938
+ }
23939
+ const error = new Error(details.reason);
23940
+ this.telemetryManager.sendEvent("upload.attempt.failed", {
23941
+ filename: metadata.filename,
23942
+ duration: metadata.duration,
23943
+ sourceType: metadata.sourceType,
23944
+ ...details
23945
+ }, error);
23946
+ },
23947
+ onUploadAttemptSucceeded: (id, details) => {
23948
+ this.uploadAttemptSuccessById.set(id, details);
23949
+ },
23950
+ onUploadRetry: (id, details) => {
23951
+ this.uploadCallbacks.onRetry?.(details);
23952
+ const metadata = this.uploadMetadataManager.getMetadata(id);
23953
+ const failureDetails = this.uploadAttemptFailureById.get(id);
23954
+ if (!(metadata && failureDetails)) {
23955
+ return;
23956
+ }
23957
+ this.telemetryManager.sendEvent("upload.retry.scheduled", {
23958
+ filename: metadata.filename,
23959
+ duration: metadata.duration,
23960
+ sourceType: metadata.sourceType,
23961
+ attempt: details.attempt,
23962
+ maxAttempts: details.maxAttempts,
23963
+ previousAttempt: failureDetails.attempt,
23964
+ loadedBytes: failureDetails.loadedBytes,
23965
+ totalBytes: failureDetails.totalBytes,
23966
+ reason: details.reason,
23967
+ retryDelayMs: details.retryDelayMs
23968
+ });
23969
+ },
23822
23970
  onUploadComplete: (id, result) => {
23823
23971
  this.isUploadInProgress = false;
23824
23972
  this.uploadCallbacks.onSuccess(result);
23825
23973
  const metadata = this.uploadMetadataManager.getMetadata(id);
23974
+ const successDetails = this.uploadAttemptSuccessById.get(id);
23826
23975
  if (metadata) {
23827
23976
  this.telemetryManager.sendEvent("upload.succeeded", {
23828
23977
  filename: metadata.filename,
23829
23978
  duration: metadata.duration,
23830
23979
  sourceType: metadata.sourceType,
23831
- recordingId: result.id
23980
+ recordingId: result.id,
23981
+ ...successDetails && successDetails
23832
23982
  });
23833
23983
  this.uploadMetadataManager.clearMetadata(id);
23834
23984
  }
23985
+ this.uploadAttemptFailureById.delete(id);
23986
+ this.uploadAttemptSuccessById.delete(id);
23835
23987
  },
23836
23988
  onUploadError: (id, error) => {
23837
23989
  this.isUploadInProgress = false;
@@ -23845,6 +23997,8 @@ class RecorderController {
23845
23997
  }, error);
23846
23998
  this.uploadMetadataManager.clearMetadata(id);
23847
23999
  }
24000
+ this.uploadAttemptFailureById.delete(id);
24001
+ this.uploadAttemptSuccessById.delete(id);
23848
24002
  }
23849
24003
  });
23850
24004
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidtreo/recorder",
3
- "version": "1.7.4",
3
+ "version": "1.7.5",
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",