@vidtreo/recorder 1.7.3 → 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 +176 -29
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1475,7 +1475,7 @@ async function buildDeviceCapabilityProfile(options = {}) {
1475
1475
 
1476
1476
  // src/core/routing/recording-route-decision.ts
1477
1477
  var DEFAULT_MIN_DEVICE_MEMORY_GB = 4;
1478
- var DEFAULT_MIN_HARDWARE_CONCURRENCY = 5;
1478
+ var DEFAULT_MIN_HARDWARE_CONCURRENCY = 4;
1479
1479
  var PLATFORM_KEYWORD_ANDROID2 = "android";
1480
1480
  var UNSUPPORTED_GUIDANCE_MESSAGE = "Recording is not supported in this browser context. Use a secure connection, go online, and try a current Chrome or Edge browser.";
1481
1481
  function hasSufficientDeviceMemory(profile, minDeviceMemory) {
@@ -1566,14 +1566,7 @@ function decideRecordingRoute(options) {
1566
1566
  const hasSmoothEncoding = hasSmoothEncodingCapability(selectedCodec);
1567
1567
  const hasPowerEfficientEncoding = hasPowerEfficientEncodingCapability(selectedCodec);
1568
1568
  const localRouteBlockers = buildLocalRouteBlockers(options.profile, hasSupportedTargetCodec, hasEnoughDeviceMemory, hasEnoughHardwareConcurrency, hasSupportedEncoding, hasSmoothEncoding, hasPowerEfficientEncoding);
1569
- if (isAndroidProfile(options.profile) && canUsePostRecordingTranscodeRoute(options.profile, hasSupportedTargetCodec)) {
1570
- return {
1571
- route: "safe-capture-post-recording-transcode",
1572
- reasonCodes: buildPostRecordingTranscodeReasonCodes(options.profile, localRouteBlockers),
1573
- selectedCodecId
1574
- };
1575
- }
1576
- if (options.profile.features.isSecureContext && options.profile.features.hasVideoEncoder && hasSupportedTargetCodec && options.profile.network.online !== false && hasEnoughDeviceMemory && hasEnoughHardwareConcurrency && hasSupportedEncoding && hasSmoothEncoding && hasPowerEfficientEncoding) {
1569
+ if (options.profile.features.isSecureContext && options.profile.features.hasVideoEncoder && hasSupportedTargetCodec && options.profile.network.online !== false && hasEnoughDeviceMemory && hasEnoughHardwareConcurrency && hasSupportedEncoding && hasSmoothEncoding) {
1577
1570
  return {
1578
1571
  route: "local-webcodecs",
1579
1572
  reasonCodes: ["webcodecs-supported", "target-codec-supported"],
@@ -6319,7 +6312,7 @@ function resolveDeviceError(input) {
6319
6312
  // package.json
6320
6313
  var package_default = {
6321
6314
  name: "@vidtreo/recorder",
6322
- version: "1.7.3",
6315
+ version: "1.7.5",
6323
6316
  type: "module",
6324
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.",
6325
6318
  main: "./dist/index.js",
@@ -6459,6 +6452,8 @@ var TELEMETRY_EVENT_CATEGORY_MAP = {
6459
6452
  "recording.stop.failed": "error",
6460
6453
  "recording.route.selected": "lifecycle",
6461
6454
  "upload.started": "performance",
6455
+ "upload.attempt.failed": "error",
6456
+ "upload.retry.scheduled": "performance",
6462
6457
  "upload.succeeded": "performance",
6463
6458
  "upload.failed": "error",
6464
6459
  "upload.route.selected": "lifecycle",
@@ -7195,6 +7190,12 @@ class UploadQueueManager {
7195
7190
  return Object.entries(expected).every(([key, value]) => actual[key] === value);
7196
7191
  }
7197
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
+ };
7198
7199
  try {
7199
7200
  await this.storageService.updateUploadStatus(upload.id, {
7200
7201
  status: "uploading"
@@ -7208,24 +7209,52 @@ class UploadQueueManager {
7208
7209
  userMetadata: upload.userMetadata,
7209
7210
  onProgress: (progress) => {
7210
7211
  this.callbacks.onUploadProgress?.(upload.id, progress);
7212
+ },
7213
+ onProgressDetails: (details) => {
7214
+ progressDetails = details;
7215
+ this.callbacks.onUploadProgressDetails?.(upload.id, details);
7211
7216
  }
7212
7217
  });
7218
+ this.callbacks.onUploadAttemptSucceeded?.(upload.id, {
7219
+ attempt,
7220
+ maxAttempts: MAX_RETRIES2,
7221
+ loadedBytes: progressDetails.loadedBytes,
7222
+ totalBytes: progressDetails.totalBytes
7223
+ });
7213
7224
  await this.storageService.deleteUpload(upload.id);
7214
7225
  this.callbacks.onUploadComplete?.(upload.id, result);
7215
7226
  return result;
7216
7227
  } catch (error) {
7217
7228
  const errorMessage = extractErrorMessage(error);
7218
7229
  const retryCount = upload.retryCount + 1;
7230
+ const willRetry = retryCount < MAX_RETRIES2;
7231
+ const retryDelayMs = willRetry ? this.calculateRetryDelay(retryCount) : null;
7219
7232
  await this.storageService.updateUploadStatus(upload.id, {
7220
7233
  status: "failed",
7221
7234
  retryCount,
7222
7235
  lastError: errorMessage
7223
7236
  });
7224
- if (retryCount >= MAX_RETRIES2) {
7225
- this.callbacks.onUploadError?.(upload.id, new Error(`Upload failed after ${MAX_RETRIES2} attempts: ${errorMessage}`));
7226
- } 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) {
7227
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
+ });
7228
7255
  this.scheduleRetry(delay2);
7256
+ } else {
7257
+ this.callbacks.onUploadError?.(upload.id, new Error(`Upload failed after ${MAX_RETRIES2} attempts: ${errorMessage}`));
7229
7258
  }
7230
7259
  if (shouldThrowOnError) {
7231
7260
  throw new Error(errorMessage);
@@ -7257,6 +7286,8 @@ class UploadQueueManager {
7257
7286
  // src/core/upload/upload-service.ts
7258
7287
  var API_UPLOAD_PATH = "/api/v1/videos/upload";
7259
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";
7260
7291
 
7261
7292
  class VideoUploadService {
7262
7293
  uploadVideo(blob, options) {
@@ -7272,27 +7303,61 @@ class VideoUploadService {
7272
7303
  return new Promise((resolve, reject) => {
7273
7304
  const xhr = new XMLHttpRequest;
7274
7305
  const uploadBlob = this.createUploadBlob(blob, options.mimeType);
7275
- if (options.onProgress) {
7276
- const onProgress = options.onProgress;
7277
- xhr.upload.addEventListener("progress", (event) => {
7278
- if (event.lengthComputable) {
7279
- const progress = event.loaded / event.total;
7280
- onProgress(progress);
7281
- }
7282
- });
7283
- }
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
+ });
7284
7349
  xhr.addEventListener("load", () => {
7285
7350
  if (xhr.status >= 200 && xhr.status <= 299) {
7286
- this.parseSuccessResponse(xhr, resolve, reject);
7351
+ this.parseSuccessResponse(xhr, resolveRequest, rejectRequest);
7287
7352
  return;
7288
7353
  }
7289
- this.parseErrorResponse(xhr, reject);
7354
+ this.parseErrorResponse(xhr, rejectRequest);
7290
7355
  });
7291
7356
  xhr.addEventListener("error", () => {
7292
- reject(new Error("Network error during upload"));
7357
+ rejectRequest(new Error("Network error during upload"));
7293
7358
  });
7294
7359
  xhr.addEventListener("abort", () => {
7295
- reject(new Error("Upload was aborted"));
7360
+ rejectRequest(new Error("Upload was aborted"));
7296
7361
  });
7297
7362
  const url = `${options.backendUrl}${API_UPLOAD_PATH}`;
7298
7363
  xhr.open("POST", url);
@@ -7308,6 +7373,19 @@ class VideoUploadService {
7308
7373
  }
7309
7374
  formData.append("metadata", JSON.stringify(metadata));
7310
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
7311
7389
  });
7312
7390
  }
7313
7391
  createUploadBlob(blob, mimeType) {
@@ -23217,6 +23295,8 @@ class RecorderController {
23217
23295
  callbacks;
23218
23296
  telemetryManager;
23219
23297
  uploadMetadataManager;
23298
+ uploadAttemptFailureById = new Map;
23299
+ uploadAttemptSuccessById = new Map;
23220
23300
  uploadQueueManager = null;
23221
23301
  isInitialized = false;
23222
23302
  isDemo = false;
@@ -23558,6 +23638,11 @@ class RecorderController {
23558
23638
  }, error);
23559
23639
  throw error;
23560
23640
  }
23641
+ let progressDetails = {
23642
+ progress: 0,
23643
+ loadedBytes: 0,
23644
+ totalBytes: 0
23645
+ };
23561
23646
  try {
23562
23647
  const result = await this.uploadService.uploadVideo(options.blob, {
23563
23648
  apiKey: options.apiKey,
@@ -23566,18 +23651,37 @@ class RecorderController {
23566
23651
  mimeType: options.mimeType,
23567
23652
  metadata: undefined,
23568
23653
  userMetadata: options.userMetadata,
23569
- onProgress: this.uploadCallbacks.onProgress
23654
+ onProgress: this.uploadCallbacks.onProgress,
23655
+ onProgressDetails: (details) => {
23656
+ progressDetails = details;
23657
+ }
23570
23658
  });
23571
23659
  this.uploadCallbacks.onSuccess(result);
23572
23660
  this.telemetryManager.sendEvent("upload.succeeded", {
23573
23661
  filename: options.filename,
23574
23662
  duration: options.duration,
23575
23663
  sourceType: options.sourceType,
23576
- recordingId: result.id
23664
+ recordingId: result.id,
23665
+ attempt: 1,
23666
+ maxAttempts: 1,
23667
+ loadedBytes: progressDetails.loadedBytes,
23668
+ totalBytes: progressDetails.totalBytes
23577
23669
  });
23578
23670
  } catch (error) {
23579
23671
  const uploadError = error instanceof Error ? error : new Error(extractErrorMessage(error));
23580
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);
23581
23685
  this.telemetryManager.sendEvent("upload.failed", {
23582
23686
  filename: options.filename,
23583
23687
  duration: options.duration,
@@ -23826,19 +23930,60 @@ class RecorderController {
23826
23930
  onUploadProgress: (_id, progress) => {
23827
23931
  this.uploadCallbacks.onProgress(progress);
23828
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
+ },
23829
23970
  onUploadComplete: (id, result) => {
23830
23971
  this.isUploadInProgress = false;
23831
23972
  this.uploadCallbacks.onSuccess(result);
23832
23973
  const metadata = this.uploadMetadataManager.getMetadata(id);
23974
+ const successDetails = this.uploadAttemptSuccessById.get(id);
23833
23975
  if (metadata) {
23834
23976
  this.telemetryManager.sendEvent("upload.succeeded", {
23835
23977
  filename: metadata.filename,
23836
23978
  duration: metadata.duration,
23837
23979
  sourceType: metadata.sourceType,
23838
- recordingId: result.id
23980
+ recordingId: result.id,
23981
+ ...successDetails && successDetails
23839
23982
  });
23840
23983
  this.uploadMetadataManager.clearMetadata(id);
23841
23984
  }
23985
+ this.uploadAttemptFailureById.delete(id);
23986
+ this.uploadAttemptSuccessById.delete(id);
23842
23987
  },
23843
23988
  onUploadError: (id, error) => {
23844
23989
  this.isUploadInProgress = false;
@@ -23852,6 +23997,8 @@ class RecorderController {
23852
23997
  }, error);
23853
23998
  this.uploadMetadataManager.clearMetadata(id);
23854
23999
  }
24000
+ this.uploadAttemptFailureById.delete(id);
24001
+ this.uploadAttemptSuccessById.delete(id);
23855
24002
  }
23856
24003
  });
23857
24004
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidtreo/recorder",
3
- "version": "1.7.3",
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",