@vidtreo/recorder 1.6.2 → 1.7.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/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export type StopRecordingResult = {
7
7
  tabVisibilityIntervals: VisibilityInterval[];
8
8
  recordingStats?: StreamRecordingStats;
9
9
  encoderAcceleration?: VideoHardwareAcceleration;
10
+ mediaRecorderDiagnostics?: Record<string, unknown>;
10
11
  };
11
12
  type CheckRecorderSupportDependency = (options: SupportCheckOptions) => Promise<SupportReport>;
12
13
  type GetCurrentTimestampDependency = () => number;
@@ -32,6 +33,8 @@ export declare class StreamRecordingState {
32
33
  private mediaRecorderStopPromise;
33
34
  private mediaRecorderStopResolve;
34
35
  private mediaRecorderStopReject;
36
+ private mediaRecorderError;
37
+ private mediaRecorderErrorTimeoutId;
35
38
  private readonly streamManager;
36
39
  private readonly dependencies;
37
40
  constructor(streamManager: StreamManager, dependencies?: Partial<StreamRecordingStateDependencies>);
@@ -60,6 +63,14 @@ export declare class StreamRecordingState {
60
63
  private stopSafeCaptureRecording;
61
64
  private resolveSafeCaptureMimeType;
62
65
  private resolvePreferredSafeCaptureMimeType;
66
+ private shouldPreferWebmSafeCapture;
67
+ private scheduleMediaRecorderErrorFinalizeTimeout;
68
+ private clearMediaRecorderErrorTimeout;
69
+ private createMediaRecorderError;
70
+ private createEmptyMediaRecorderError;
71
+ private updateMediaRecorderErrorDiagnostics;
72
+ private buildMediaRecorderDiagnostics;
73
+ private getMediaRecorderChunkSize;
63
74
  private resetPauseState;
64
75
  private resolveTabVisibilityOverlayText;
65
76
  private setupVisibilityUpdates;
@@ -683,9 +694,17 @@ export type VideoEncoderProbeResult = {
683
694
  id: string;
684
695
  config: VideoEncoderConfig;
685
696
  supported: boolean;
697
+ encodingCapability?: VideoEncodingCapabilityProbeResult;
686
698
  errorCode?: "codec-probe-error";
687
699
  errorMessage?: string;
688
700
  };
701
+ export type VideoEncodingCapabilityProbeResult = {
702
+ supported: boolean;
703
+ smooth: boolean;
704
+ powerEfficient: boolean;
705
+ errorCode?: "encoding-capability-probe-error";
706
+ errorMessage?: string;
707
+ };
689
708
  export type DeviceCapabilityProfile = {
690
709
  browser: {
691
710
  name: string;
@@ -712,12 +731,14 @@ export type DeviceCapabilityProfile = {
712
731
  codecProbeResults: VideoEncoderProbeResult[];
713
732
  };
714
733
  export type VideoEncoderSupportProbe = (config: VideoEncoderConfig) => Promise<VideoEncoderSupport>;
734
+ export type VideoEncodingCapabilityProbe = (config: VideoEncoderConfig) => Promise<MediaCapabilitiesEncodingInfo | null>;
715
735
  type DeviceCapabilityProfileDependencies = {
716
736
  navigatorProvider: CapabilityNavigatorProvider | null;
717
737
  secureContextProvider: () => boolean;
718
738
  mediaRecorderAvailabilityProvider: () => boolean;
719
739
  videoEncoderAvailabilityProvider: () => boolean;
720
740
  codecSupportProbe: VideoEncoderSupportProbe;
741
+ encodingCapabilityProbe: VideoEncodingCapabilityProbe;
721
742
  };
722
743
  export type BuildDeviceCapabilityProfileOptions = {
723
744
  codecCandidates?: VideoEncoderConfigCandidate[];
@@ -1326,6 +1347,7 @@ export declare class TelemetryClient {
1326
1347
  private isSafeCapabilityValue;
1327
1348
  private isRouteSourceType;
1328
1349
  private buildErrorProperties;
1350
+ private buildMediaRecorderErrorProperties;
1329
1351
  private buildFingerprint;
1330
1352
  private buildContext;
1331
1353
  private buildError;
@@ -2443,7 +2465,7 @@ export declare function deserializeBitrate(bitrate: number | string | undefined)
2443
2465
  import type { DeviceCapabilityProfile } from "../capabilities/device-capability-profile";
2444
2466
  export type RecordingCapabilityProfile = DeviceCapabilityProfile;
2445
2467
  export type RecordingRoute = "local-webcodecs" | "safe-capture-post-recording-transcode" | "unsupported-with-guidance";
2446
- export type RecordingRouteReasonCode = "webcodecs-supported" | "webcodecs-unavailable" | "android-post-recording-transcode" | "target-codec-supported" | "target-codec-unsupported" | "insufficient-device-memory" | "insufficient-hardware-concurrency" | "insecure-context" | "mediarecorder-supported" | "mediarecorder-unavailable" | "offline";
2468
+ export type RecordingRouteReasonCode = "webcodecs-supported" | "webcodecs-unavailable" | "target-codec-supported" | "target-codec-unsupported" | "encoding-capability-unsupported" | "encoding-not-smooth" | "encoding-not-power-efficient" | "insufficient-device-memory" | "insufficient-hardware-concurrency" | "insecure-context" | "mediarecorder-supported" | "mediarecorder-unavailable" | "android-post-recording-transcode" | "offline";
2447
2469
  export type RecordingRouteDecision = {
2448
2470
  route: RecordingRoute;
2449
2471
  reasonCodes: RecordingRouteReasonCode[];
@@ -2466,7 +2488,8 @@ export declare function decideRecordingRoute(options: DecideRecordingRouteOption
2466
2488
  export declare const ERROR_RECORDING_STOP_NOT_READY = "recording.stop-not-ready";
2467
2489
  export declare const ERROR_RECORDING_FINALIZE_NOT_READY = "recording.finalize-not-ready";
2468
2490
  export declare const ERROR_RECORDING_FINALIZE_TIMEOUT = "recording.finalize-timeout";
2469
- export type RecordingLifecycleErrorCode = typeof ERROR_RECORDING_STOP_NOT_READY | typeof ERROR_RECORDING_FINALIZE_NOT_READY | typeof ERROR_RECORDING_FINALIZE_TIMEOUT;
2491
+ export declare const ERROR_RECORDING_UPLOAD_IN_PROGRESS = "recording.upload-in-progress";
2492
+ export type RecordingLifecycleErrorCode = typeof ERROR_RECORDING_STOP_NOT_READY | typeof ERROR_RECORDING_FINALIZE_NOT_READY | typeof ERROR_RECORDING_FINALIZE_TIMEOUT | typeof ERROR_RECORDING_UPLOAD_IN_PROGRESS;
2470
2493
  export type RecordingLifecycleError = Error & {
2471
2494
  code: RecordingLifecycleErrorCode;
2472
2495
  isRecoverable: true;
@@ -2759,6 +2782,7 @@ export declare class RecorderController {
2759
2782
  private isDestroyed;
2760
2783
  private enableTabVisibilityOverlay;
2761
2784
  private tabVisibilityOverlayText;
2785
+ private isUploadInProgress;
2762
2786
  private recordingWarmupTimeoutId;
2763
2787
  private audioTelemetryUnsub;
2764
2788
  constructor(callbacks?: RecorderCallbacks);
@@ -2769,6 +2793,7 @@ export declare class RecorderController {
2769
2793
  switchAudioDevice(deviceId: string | null): Promise<MediaStream>;
2770
2794
  startRecording(): Promise<void>;
2771
2795
  stopRecording(): Promise<Blob>;
2796
+ private shouldStopStreamAfterStopFailure;
2772
2797
  private sendAudioMissingTelemetry;
2773
2798
  getTabVisibilityOverlayConfig(): {
2774
2799
  enabled: boolean;
@@ -2788,6 +2813,7 @@ export declare class RecorderController {
2788
2813
  stopAudioLevelTracking(): void;
2789
2814
  getAudioLevel(): number;
2790
2815
  uploadVideo(blob: Blob, apiKey: string, backendUrl: string, metadata: Record<string, unknown>): Promise<void>;
2816
+ private uploadVideoAfterStatusClear;
2791
2817
  private uploadDirectlyWithoutQueue;
2792
2818
  getStream(): MediaStream | null;
2793
2819
  isConfigReady(): boolean;
@@ -2802,6 +2828,7 @@ export declare class RecorderController {
2802
2828
  getDeviceManager(): DeviceManager;
2803
2829
  getConfig(): Promise<TranscodeConfig>;
2804
2830
  getUploadService(): VideoUploadService | null;
2831
+ isUploading(): boolean;
2805
2832
  isRecording(): boolean;
2806
2833
  isActive(): boolean;
2807
2834
  isAudioReady(): boolean;
package/dist/index.js CHANGED
@@ -1321,6 +1321,48 @@ function isMediaRecorderAvailable() {
1321
1321
  function isVideoEncoderAvailable() {
1322
1322
  return typeof globalThis.VideoEncoder !== "undefined";
1323
1323
  }
1324
+ function getGlobalMediaCapabilities() {
1325
+ if (typeof navigator === "undefined") {
1326
+ return null;
1327
+ }
1328
+ return navigator.mediaCapabilities;
1329
+ }
1330
+ function resolveEncodingContentType(codec) {
1331
+ const normalizedCodec = codec.toLowerCase();
1332
+ if (normalizedCodec.startsWith("avc1") || normalizedCodec.startsWith("av01") || normalizedCodec.startsWith("hvc1") || normalizedCodec.startsWith("hev1")) {
1333
+ return `video/mp4;codecs="${codec}"`;
1334
+ }
1335
+ if (normalizedCodec === "vp8" || normalizedCodec.startsWith("vp09") || normalizedCodec.startsWith("vp9")) {
1336
+ return `video/webm;codecs="${codec}"`;
1337
+ }
1338
+ return null;
1339
+ }
1340
+ function probeVideoEncodingCapability(config) {
1341
+ const mediaCapabilities = getGlobalMediaCapabilities();
1342
+ if (!mediaCapabilities) {
1343
+ return Promise.resolve(null);
1344
+ }
1345
+ const contentType = resolveEncodingContentType(config.codec);
1346
+ if (contentType === null) {
1347
+ return Promise.resolve(null);
1348
+ }
1349
+ if (!(typeof config.bitrate === "number")) {
1350
+ return Promise.resolve(null);
1351
+ }
1352
+ if (!(typeof config.framerate === "number")) {
1353
+ return Promise.resolve(null);
1354
+ }
1355
+ return mediaCapabilities.encodingInfo({
1356
+ type: "record",
1357
+ video: {
1358
+ contentType,
1359
+ width: config.width,
1360
+ height: config.height,
1361
+ bitrate: config.bitrate,
1362
+ framerate: config.framerate
1363
+ }
1364
+ });
1365
+ }
1324
1366
  function probeVideoEncoderConfig(config) {
1325
1367
  if (typeof globalThis.VideoEncoder === "undefined") {
1326
1368
  return Promise.resolve({
@@ -1336,7 +1378,8 @@ function resolveDependencies(dependencies) {
1336
1378
  secureContextProvider: dependencies?.secureContextProvider ?? isSecureContextAvailable,
1337
1379
  mediaRecorderAvailabilityProvider: dependencies?.mediaRecorderAvailabilityProvider ?? isMediaRecorderAvailable,
1338
1380
  videoEncoderAvailabilityProvider: dependencies?.videoEncoderAvailabilityProvider ?? isVideoEncoderAvailable,
1339
- codecSupportProbe: dependencies?.codecSupportProbe ?? probeVideoEncoderConfig
1381
+ codecSupportProbe: dependencies?.codecSupportProbe ?? probeVideoEncoderConfig,
1382
+ encodingCapabilityProbe: dependencies?.encodingCapabilityProbe ?? probeVideoEncodingCapability
1340
1383
  };
1341
1384
  }
1342
1385
  function parseUserAgent(userAgent) {
@@ -1362,14 +1405,22 @@ function normalizeProbeErrorMessage(error) {
1362
1405
  }
1363
1406
  return String(error);
1364
1407
  }
1365
- async function probeCodecCandidate(candidate, codecSupportProbe) {
1408
+ async function probeCodecCandidate(candidate, codecSupportProbe, encodingCapabilityProbe) {
1366
1409
  try {
1367
1410
  const result = await codecSupportProbe(candidate.config);
1368
- return {
1411
+ const encodingCapability = await probeEncodingCapability(candidate.config, encodingCapabilityProbe);
1412
+ const probeResult = {
1369
1413
  id: candidate.id,
1370
1414
  config: candidate.config,
1371
1415
  supported: result.supported === true
1372
1416
  };
1417
+ if (encodingCapability === undefined) {
1418
+ return probeResult;
1419
+ }
1420
+ return {
1421
+ ...probeResult,
1422
+ encodingCapability
1423
+ };
1373
1424
  } catch (error) {
1374
1425
  return {
1375
1426
  id: candidate.id,
@@ -1380,10 +1431,28 @@ async function probeCodecCandidate(candidate, codecSupportProbe) {
1380
1431
  };
1381
1432
  }
1382
1433
  }
1434
+ function probeEncodingCapability(config, encodingCapabilityProbe) {
1435
+ return Promise.resolve().then(() => encodingCapabilityProbe(config)).then((result) => {
1436
+ if (result === null) {
1437
+ return;
1438
+ }
1439
+ return {
1440
+ supported: result.supported === true,
1441
+ smooth: result.smooth === true,
1442
+ powerEfficient: result.powerEfficient === true
1443
+ };
1444
+ }).catch((error) => ({
1445
+ supported: false,
1446
+ smooth: false,
1447
+ powerEfficient: false,
1448
+ errorCode: "encoding-capability-probe-error",
1449
+ errorMessage: normalizeProbeErrorMessage(error)
1450
+ }));
1451
+ }
1383
1452
  async function buildDeviceCapabilityProfile(options = {}) {
1384
1453
  const dependencies = resolveDependencies(options.dependencies);
1385
1454
  const { browser, os } = parseUserAgent(dependencies.navigatorProvider?.userAgent);
1386
- const codecProbeResults = await Promise.all((options.codecCandidates ?? []).map((candidate) => probeCodecCandidate(candidate, dependencies.codecSupportProbe)));
1455
+ const codecProbeResults = await Promise.all((options.codecCandidates ?? []).map((candidate) => probeCodecCandidate(candidate, dependencies.codecSupportProbe, dependencies.encodingCapabilityProbe)));
1387
1456
  return {
1388
1457
  browser,
1389
1458
  os,
@@ -1406,7 +1475,8 @@ async function buildDeviceCapabilityProfile(options = {}) {
1406
1475
 
1407
1476
  // src/core/routing/recording-route-decision.ts
1408
1477
  var DEFAULT_MIN_DEVICE_MEMORY_GB = 4;
1409
- var DEFAULT_MIN_HARDWARE_CONCURRENCY = 4;
1478
+ var DEFAULT_MIN_HARDWARE_CONCURRENCY = 5;
1479
+ var PLATFORM_KEYWORD_ANDROID2 = "android";
1410
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.";
1411
1481
  function hasSufficientDeviceMemory(profile, minDeviceMemory) {
1412
1482
  const deviceMemory = profile.resources.deviceMemory;
@@ -1416,18 +1486,14 @@ function hasSufficientHardwareConcurrency(profile, minHardwareConcurrency) {
1416
1486
  const hardwareConcurrency = profile.resources.hardwareConcurrency;
1417
1487
  return hardwareConcurrency === undefined || hardwareConcurrency >= minHardwareConcurrency;
1418
1488
  }
1419
- function findSupportedTargetCodecId(profile, targetCodecIds) {
1489
+ function findSupportedTargetCodec(profile, targetCodecIds) {
1420
1490
  const targetCodecIdSet = new Set(targetCodecIds);
1421
- const supportedCodec = profile.codecProbeResults.find((result) => targetCodecIdSet.has(result.id) && result.supported);
1422
- return supportedCodec?.id;
1491
+ return profile.codecProbeResults.find((result) => targetCodecIdSet.has(result.id) && result.supported);
1423
1492
  }
1424
1493
  function canUsePostRecordingTranscodeRoute(profile, hasSupportedTargetCodec) {
1425
1494
  return profile.features.isSecureContext && profile.features.hasMediaRecorder && profile.features.hasVideoEncoder && hasSupportedTargetCodec && profile.network.online !== false;
1426
1495
  }
1427
- function isAndroidBrowser(profile) {
1428
- return profile.os.name.toLowerCase() === "android";
1429
- }
1430
- function buildLocalRouteBlockers(profile, hasSupportedTargetCodec, hasEnoughDeviceMemory, hasEnoughHardwareConcurrency) {
1496
+ function buildLocalRouteBlockers(profile, hasSupportedTargetCodec, hasEnoughDeviceMemory, hasEnoughHardwareConcurrency, hasSupportedEncodingCapability, hasSmoothEncoding, hasPowerEfficientEncoding) {
1431
1497
  const reasonCodes = [];
1432
1498
  if (!profile.features.isSecureContext) {
1433
1499
  reasonCodes.push("insecure-context");
@@ -1444,8 +1510,14 @@ function buildLocalRouteBlockers(profile, hasSupportedTargetCodec, hasEnoughDevi
1444
1510
  if (!hasEnoughHardwareConcurrency) {
1445
1511
  reasonCodes.push("insufficient-hardware-concurrency");
1446
1512
  }
1447
- if (isAndroidBrowser(profile)) {
1448
- reasonCodes.push("android-post-recording-transcode");
1513
+ if (!hasSupportedEncodingCapability) {
1514
+ reasonCodes.push("encoding-capability-unsupported");
1515
+ }
1516
+ if (!hasSmoothEncoding) {
1517
+ reasonCodes.push("encoding-not-smooth");
1518
+ }
1519
+ if (!hasPowerEfficientEncoding) {
1520
+ reasonCodes.push("encoding-not-power-efficient");
1449
1521
  }
1450
1522
  return reasonCodes;
1451
1523
  }
@@ -1459,15 +1531,49 @@ function buildFallbackRouteBlockers(profile) {
1459
1531
  }
1460
1532
  return reasonCodes;
1461
1533
  }
1534
+ function isAndroidProfile(profile) {
1535
+ return profile.os.name.toLowerCase().includes(PLATFORM_KEYWORD_ANDROID2);
1536
+ }
1537
+ function buildPostRecordingTranscodeReasonCodes(profile, localRouteBlockers) {
1538
+ const reasonCodes = [...localRouteBlockers];
1539
+ if (isAndroidProfile(profile)) {
1540
+ reasonCodes.push("android-post-recording-transcode");
1541
+ }
1542
+ reasonCodes.push("mediarecorder-supported");
1543
+ return reasonCodes;
1544
+ }
1545
+ function hasSupportedEncodingCapability(selectedCodec) {
1546
+ const encodingCapability = selectedCodec?.encodingCapability;
1547
+ return encodingCapability === undefined || encodingCapability.errorCode !== undefined || encodingCapability.supported === true;
1548
+ }
1549
+ function hasSmoothEncodingCapability(selectedCodec) {
1550
+ const encodingCapability = selectedCodec?.encodingCapability;
1551
+ return encodingCapability === undefined || encodingCapability.errorCode !== undefined || encodingCapability.supported !== true || encodingCapability.smooth === true;
1552
+ }
1553
+ function hasPowerEfficientEncodingCapability(selectedCodec) {
1554
+ const encodingCapability = selectedCodec?.encodingCapability;
1555
+ return encodingCapability === undefined || encodingCapability.errorCode !== undefined || encodingCapability.supported !== true || encodingCapability.powerEfficient === true;
1556
+ }
1462
1557
  function decideRecordingRoute(options) {
1463
1558
  const minDeviceMemory = options.resourceThresholds?.minDeviceMemory ?? DEFAULT_MIN_DEVICE_MEMORY_GB;
1464
1559
  const minHardwareConcurrency = options.resourceThresholds?.minHardwareConcurrency ?? DEFAULT_MIN_HARDWARE_CONCURRENCY;
1465
- const selectedCodecId = findSupportedTargetCodecId(options.profile, options.targetCodecIds);
1560
+ const selectedCodec = findSupportedTargetCodec(options.profile, options.targetCodecIds);
1561
+ const selectedCodecId = selectedCodec?.id;
1466
1562
  const hasSupportedTargetCodec = selectedCodecId !== undefined;
1467
1563
  const hasEnoughDeviceMemory = hasSufficientDeviceMemory(options.profile, minDeviceMemory);
1468
1564
  const hasEnoughHardwareConcurrency = hasSufficientHardwareConcurrency(options.profile, minHardwareConcurrency);
1469
- const localRouteBlockers = buildLocalRouteBlockers(options.profile, hasSupportedTargetCodec, hasEnoughDeviceMemory, hasEnoughHardwareConcurrency);
1470
- if (options.profile.features.isSecureContext && options.profile.features.hasVideoEncoder && hasSupportedTargetCodec && options.profile.network.online !== false && !isAndroidBrowser(options.profile) && hasEnoughDeviceMemory && hasEnoughHardwareConcurrency) {
1565
+ const hasSupportedEncoding = hasSupportedEncodingCapability(selectedCodec);
1566
+ const hasSmoothEncoding = hasSmoothEncodingCapability(selectedCodec);
1567
+ const hasPowerEfficientEncoding = hasPowerEfficientEncodingCapability(selectedCodec);
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) {
1471
1577
  return {
1472
1578
  route: "local-webcodecs",
1473
1579
  reasonCodes: ["webcodecs-supported", "target-codec-supported"],
@@ -1477,7 +1583,7 @@ function decideRecordingRoute(options) {
1477
1583
  if (canUsePostRecordingTranscodeRoute(options.profile, hasSupportedTargetCodec)) {
1478
1584
  return {
1479
1585
  route: "safe-capture-post-recording-transcode",
1480
- reasonCodes: [...localRouteBlockers, "mediarecorder-supported"],
1586
+ reasonCodes: buildPostRecordingTranscodeReasonCodes(options.profile, localRouteBlockers),
1481
1587
  selectedCodecId
1482
1588
  };
1483
1589
  }
@@ -4952,6 +5058,7 @@ class StreamManager {
4952
5058
  var ERROR_RECORDING_STOP_NOT_READY = "recording.stop-not-ready";
4953
5059
  var ERROR_RECORDING_FINALIZE_NOT_READY = "recording.finalize-not-ready";
4954
5060
  var ERROR_RECORDING_FINALIZE_TIMEOUT = "recording.finalize-timeout";
5061
+ var ERROR_RECORDING_UPLOAD_IN_PROGRESS = "recording.upload-in-progress";
4955
5062
  function createRecordingLifecycleError(code, message, details) {
4956
5063
  const error = new Error(`${message} [${code}]`);
4957
5064
  error.code = code;
@@ -4978,7 +5085,7 @@ function normalizeRecordingLifecycleError(error) {
4978
5085
  }
4979
5086
  function isRecordingLifecycleError(error) {
4980
5087
  const errorWithCode = error;
4981
- return errorWithCode.code === ERROR_RECORDING_STOP_NOT_READY || errorWithCode.code === ERROR_RECORDING_FINALIZE_NOT_READY || errorWithCode.code === ERROR_RECORDING_FINALIZE_TIMEOUT;
5088
+ return errorWithCode.code === ERROR_RECORDING_STOP_NOT_READY || errorWithCode.code === ERROR_RECORDING_FINALIZE_NOT_READY || errorWithCode.code === ERROR_RECORDING_FINALIZE_TIMEOUT || errorWithCode.code === ERROR_RECORDING_UPLOAD_IN_PROGRESS;
4982
5089
  }
4983
5090
 
4984
5091
  // src/core/utils/formatters.ts
@@ -5163,6 +5270,11 @@ var ANDROID_SAFE_CAPTURE_CONSTRAINTS = {
5163
5270
  height: { ideal: 720, max: 720 },
5164
5271
  frameRate: { ideal: 24, max: 24 }
5165
5272
  };
5273
+ var SAFE_CAPTURE_TIMESLICE_MS = 1000;
5274
+ var MEDIA_RECORDER_ERROR_FINALIZE_TIMEOUT_MS = 5000;
5275
+ var ANDROID_POST_RECORDING_TRANSCODE_REASON = "android-post-recording-transcode";
5276
+ var MEDIA_RECORDER_ERROR_CODE = "recording.mediarecorder-error";
5277
+ var MEDIA_RECORDER_EMPTY_ERROR_CODE = "recording.mediarecorder-empty";
5166
5278
  function createUnsupportedRouteError(routeDecision) {
5167
5279
  const guidance = routeDecision.guidance?.message ?? "This browser or device cannot safely record video. Try a supported browser or upload an existing video file.";
5168
5280
  const error = new Error(guidance);
@@ -5194,6 +5306,8 @@ class StreamRecordingState {
5194
5306
  mediaRecorderStopPromise = null;
5195
5307
  mediaRecorderStopResolve = null;
5196
5308
  mediaRecorderStopReject = null;
5309
+ mediaRecorderError = null;
5310
+ mediaRecorderErrorTimeoutId = null;
5197
5311
  streamManager;
5198
5312
  dependencies;
5199
5313
  constructor(streamManager, dependencies) {
@@ -5366,9 +5480,11 @@ class StreamRecordingState {
5366
5480
  }
5367
5481
  let result;
5368
5482
  if (this.activeRouteDecision.route === "safe-capture-post-recording-transcode") {
5483
+ const safeCaptureResult = await this.stopSafeCaptureRecording();
5369
5484
  logger.debug("[StreamRecordingState] Finalizing MediaRecorder capture");
5370
5485
  result = {
5371
- blob: await this.stopSafeCaptureRecording()
5486
+ blob: safeCaptureResult.blob,
5487
+ mediaRecorderDiagnostics: safeCaptureResult.diagnostics
5372
5488
  };
5373
5489
  } else {
5374
5490
  logger.debug("[StreamRecordingState] Finalizing stream processor");
@@ -5387,7 +5503,8 @@ class StreamRecordingState {
5387
5503
  blob: result.blob,
5388
5504
  tabVisibilityIntervals,
5389
5505
  recordingStats: result.recordingStats,
5390
- encoderAcceleration: result.encoderAcceleration
5506
+ encoderAcceleration: result.encoderAcceleration,
5507
+ mediaRecorderDiagnostics: result.mediaRecorderDiagnostics
5391
5508
  };
5392
5509
  }
5393
5510
  pauseRecording() {
@@ -5398,6 +5515,9 @@ class StreamRecordingState {
5398
5515
  if (this.tabVisibilityTracker) {
5399
5516
  this.tabVisibilityTracker.pause();
5400
5517
  }
5518
+ if (this.mediaRecorder?.state === "recording") {
5519
+ this.mediaRecorder.pause?.();
5520
+ }
5401
5521
  if (this.streamProcessor && this.isRecording()) {
5402
5522
  this.streamProcessor.pause();
5403
5523
  }
@@ -5411,6 +5531,9 @@ class StreamRecordingState {
5411
5531
  if (this.tabVisibilityTracker) {
5412
5532
  this.tabVisibilityTracker.resume();
5413
5533
  }
5534
+ if (this.mediaRecorder?.state === "paused") {
5535
+ this.mediaRecorder.resume?.();
5536
+ }
5414
5537
  this.startRecordingTimer();
5415
5538
  if (this.streamProcessor && this.isRecording()) {
5416
5539
  this.streamProcessor.resume();
@@ -5510,6 +5633,7 @@ class StreamRecordingState {
5510
5633
  }
5511
5634
  await this.applySafeCaptureTrackConstraints(mediaStream);
5512
5635
  this.mediaRecorderChunks = [];
5636
+ this.mediaRecorderError = null;
5513
5637
  const preferredMimeType = this.resolvePreferredSafeCaptureMimeType(config);
5514
5638
  const recorderOptions = preferredMimeType === undefined ? undefined : { mimeType: preferredMimeType };
5515
5639
  const recorder = new MediaRecorder(mediaStream, recorderOptions);
@@ -5518,20 +5642,34 @@ class StreamRecordingState {
5518
5642
  this.mediaRecorderStopResolve = resolve;
5519
5643
  this.mediaRecorderStopReject = reject;
5520
5644
  });
5645
+ this.mediaRecorderStopPromise.catch(() => {
5646
+ return;
5647
+ });
5521
5648
  recorder.ondataavailable = (event) => {
5522
5649
  if (event.data.size > 0) {
5523
5650
  this.mediaRecorderChunks.push(event.data);
5524
5651
  }
5525
5652
  };
5526
5653
  recorder.onstop = () => {
5654
+ this.clearMediaRecorderErrorTimeout();
5655
+ if (this.mediaRecorderError !== null) {
5656
+ this.updateMediaRecorderErrorDiagnostics(recorder);
5657
+ this.mediaRecorderStopReject?.(this.mediaRecorderError);
5658
+ return;
5659
+ }
5527
5660
  const mimeType = recorder.mimeType || this.resolveSafeCaptureMimeType();
5528
5661
  const blob = new Blob(this.mediaRecorderChunks, { type: mimeType });
5529
- this.mediaRecorderStopResolve?.(blob);
5662
+ const diagnostics = this.buildMediaRecorderDiagnostics(recorder);
5663
+ if (blob.size > 0) {
5664
+ this.mediaRecorderStopResolve?.({ blob, diagnostics });
5665
+ return;
5666
+ }
5667
+ this.mediaRecorderStopReject?.(this.createEmptyMediaRecorderError(recorder));
5530
5668
  };
5531
- recorder.onerror = () => {
5532
- this.mediaRecorderStopReject?.(new Error("Browser recording failed before upload could start"));
5669
+ recorder.onerror = (event) => {
5670
+ this.mediaRecorderError = this.createMediaRecorderError(event, recorder);
5533
5671
  };
5534
- recorder.start();
5672
+ recorder.start(SAFE_CAPTURE_TIMESLICE_MS);
5535
5673
  }
5536
5674
  async applySafeCaptureTrackConstraints(mediaStream) {
5537
5675
  const constraints = this.resolveSafeCaptureVideoConstraints();
@@ -5549,10 +5687,10 @@ class StreamRecordingState {
5549
5687
  }
5550
5688
  resolveSafeCaptureVideoConstraints() {
5551
5689
  const reasonCodes = this.activeRouteDecision.reasonCodes;
5552
- if (reasonCodes.includes("insufficient-device-memory") || reasonCodes.includes("insufficient-hardware-concurrency")) {
5690
+ if (reasonCodes.includes("insufficient-device-memory") || reasonCodes.includes("insufficient-hardware-concurrency") || reasonCodes.includes("encoding-capability-unsupported") || reasonCodes.includes("encoding-not-smooth") || reasonCodes.includes("encoding-not-power-efficient")) {
5553
5691
  return LOW_RESOURCE_SAFE_CAPTURE_CONSTRAINTS;
5554
5692
  }
5555
- if (reasonCodes.includes("android-post-recording-transcode")) {
5693
+ if (reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON)) {
5556
5694
  return ANDROID_SAFE_CAPTURE_CONSTRAINTS;
5557
5695
  }
5558
5696
  return null;
@@ -5566,6 +5704,7 @@ class StreamRecordingState {
5566
5704
  if (recorder.state !== "inactive") {
5567
5705
  recorder.stop();
5568
5706
  }
5707
+ this.scheduleMediaRecorderErrorFinalizeTimeout(recorder);
5569
5708
  try {
5570
5709
  return await stopPromise;
5571
5710
  } finally {
@@ -5574,6 +5713,8 @@ class StreamRecordingState {
5574
5713
  this.mediaRecorderStopPromise = null;
5575
5714
  this.mediaRecorderStopResolve = null;
5576
5715
  this.mediaRecorderStopReject = null;
5716
+ this.mediaRecorderError = null;
5717
+ this.clearMediaRecorderErrorTimeout();
5577
5718
  this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
5578
5719
  }
5579
5720
  }
@@ -5589,17 +5730,110 @@ class StreamRecordingState {
5589
5730
  }
5590
5731
  return MediaRecorder.isTypeSupported(mimeType);
5591
5732
  };
5592
- const candidateMimeTypes = config.format === "mp4" ? [
5593
- "video/mp4;codecs=avc1.42E01E,mp4a.40.2",
5594
- "video/mp4;codecs=avc1.42001f,mp4a.40.2",
5595
- "video/mp4"
5596
- ] : [
5733
+ const webmCandidateMimeTypes = [
5597
5734
  "video/webm;codecs=vp8,opus",
5598
5735
  "video/webm;codecs=vp9,opus",
5599
5736
  "video/webm"
5600
5737
  ];
5738
+ const mp4CandidateMimeTypes = [
5739
+ "video/mp4;codecs=avc1.42E01E,mp4a.40.2",
5740
+ "video/mp4;codecs=avc1.42001f,mp4a.40.2",
5741
+ "video/mp4"
5742
+ ];
5743
+ let candidateMimeTypes = mp4CandidateMimeTypes;
5744
+ if (this.shouldPreferWebmSafeCapture(config)) {
5745
+ candidateMimeTypes = [
5746
+ ...webmCandidateMimeTypes,
5747
+ ...mp4CandidateMimeTypes
5748
+ ];
5749
+ } else if (config.format !== "mp4") {
5750
+ candidateMimeTypes = webmCandidateMimeTypes;
5751
+ }
5601
5752
  return candidateMimeTypes.find(canUseMimeType);
5602
5753
  }
5754
+ shouldPreferWebmSafeCapture(config) {
5755
+ if (config.format !== "mp4") {
5756
+ return false;
5757
+ }
5758
+ return this.activeRouteDecision.reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON);
5759
+ }
5760
+ scheduleMediaRecorderErrorFinalizeTimeout(recorder) {
5761
+ if (this.mediaRecorderErrorTimeoutId !== null) {
5762
+ return;
5763
+ }
5764
+ this.mediaRecorderErrorTimeoutId = window.setTimeout(() => {
5765
+ if (this.mediaRecorderError !== null) {
5766
+ this.mediaRecorderStopReject?.(this.mediaRecorderError);
5767
+ return;
5768
+ }
5769
+ this.mediaRecorderStopReject?.(this.createEmptyMediaRecorderError(recorder));
5770
+ }, MEDIA_RECORDER_ERROR_FINALIZE_TIMEOUT_MS);
5771
+ }
5772
+ clearMediaRecorderErrorTimeout() {
5773
+ if (this.mediaRecorderErrorTimeoutId === null) {
5774
+ return;
5775
+ }
5776
+ window.clearTimeout(this.mediaRecorderErrorTimeoutId);
5777
+ this.mediaRecorderErrorTimeoutId = null;
5778
+ }
5779
+ createMediaRecorderError(event, recorder) {
5780
+ const mediaRecorderEvent = event;
5781
+ const browserError = mediaRecorderEvent.error;
5782
+ const error = new Error("Browser recording failed before upload could start");
5783
+ error.name = "MediaRecorderRecordingError";
5784
+ error.code = MEDIA_RECORDER_ERROR_CODE;
5785
+ error.mediaRecorderMimeType = recorder.mimeType;
5786
+ error.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
5787
+ error.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
5788
+ if (browserError?.name) {
5789
+ error.mediaRecorderErrorName = browserError.name;
5790
+ }
5791
+ if (browserError?.message) {
5792
+ error.mediaRecorderErrorMessage = browserError.message;
5793
+ }
5794
+ return error;
5795
+ }
5796
+ createEmptyMediaRecorderError(recorder) {
5797
+ const error = new Error("Browser recording produced no data before upload could start");
5798
+ error.name = "MediaRecorderEmptyDataError";
5799
+ error.code = MEDIA_RECORDER_EMPTY_ERROR_CODE;
5800
+ error.mediaRecorderMimeType = recorder.mimeType;
5801
+ error.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
5802
+ error.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
5803
+ if (this.mediaRecorderError?.mediaRecorderErrorName) {
5804
+ error.mediaRecorderErrorName = this.mediaRecorderError.mediaRecorderErrorName;
5805
+ }
5806
+ if (this.mediaRecorderError?.mediaRecorderErrorMessage) {
5807
+ error.mediaRecorderErrorMessage = this.mediaRecorderError.mediaRecorderErrorMessage;
5808
+ }
5809
+ return error;
5810
+ }
5811
+ updateMediaRecorderErrorDiagnostics(recorder) {
5812
+ if (this.mediaRecorderError === null) {
5813
+ return;
5814
+ }
5815
+ this.mediaRecorderError.mediaRecorderMimeType = recorder.mimeType;
5816
+ this.mediaRecorderError.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
5817
+ this.mediaRecorderError.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
5818
+ }
5819
+ buildMediaRecorderDiagnostics(recorder) {
5820
+ const diagnostics = {
5821
+ mediaRecorderMimeType: recorder.mimeType,
5822
+ mediaRecorderChunkCount: this.mediaRecorderChunks.length,
5823
+ mediaRecorderChunkSize: this.getMediaRecorderChunkSize(),
5824
+ mediaRecorderHadError: this.mediaRecorderError !== null
5825
+ };
5826
+ if (this.mediaRecorderError?.mediaRecorderErrorName) {
5827
+ diagnostics.mediaRecorderErrorName = this.mediaRecorderError.mediaRecorderErrorName;
5828
+ }
5829
+ if (this.mediaRecorderError?.mediaRecorderErrorMessage) {
5830
+ diagnostics.mediaRecorderErrorMessage = this.mediaRecorderError.mediaRecorderErrorMessage;
5831
+ }
5832
+ return diagnostics;
5833
+ }
5834
+ getMediaRecorderChunkSize() {
5835
+ return this.mediaRecorderChunks.reduce((total, chunk) => total + chunk.size, 0);
5836
+ }
5603
5837
  resetPauseState() {
5604
5838
  this.totalPausedTime = 0;
5605
5839
  this.pauseStartTime = null;
@@ -5679,6 +5913,8 @@ class StreamRecordingState {
5679
5913
  this.mediaRecorderStopPromise = null;
5680
5914
  this.mediaRecorderStopResolve = null;
5681
5915
  this.mediaRecorderStopReject = null;
5916
+ this.mediaRecorderError = null;
5917
+ this.clearMediaRecorderErrorTimeout();
5682
5918
  this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
5683
5919
  if (this.streamProcessor) {
5684
5920
  this.streamProcessor.destroy();
@@ -5826,6 +6062,8 @@ var RECORDING_CODEC_UNSUPPORTED_CODES = new Set([
5826
6062
  ]);
5827
6063
  var RECORDING_SERVER_TRANSCODE_FALLBACK_CODES = new Set([
5828
6064
  "recording.post-recording-transcode-fallback",
6065
+ "recording.mediarecorder-empty",
6066
+ "recording.mediarecorder-error",
5829
6067
  "transcode.post-recording-fallback",
5830
6068
  "route.post-recording-transcode-fallback"
5831
6069
  ]);
@@ -6061,7 +6299,7 @@ function resolveDeviceError(input) {
6061
6299
  // package.json
6062
6300
  var package_default = {
6063
6301
  name: "@vidtreo/recorder",
6064
- version: "1.6.2",
6302
+ version: "1.7.0",
6065
6303
  type: "module",
6066
6304
  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.",
6067
6305
  main: "./dist/index.js",
@@ -6146,6 +6384,13 @@ var MAX_RETRY_ATTEMPTS = 3;
6146
6384
  var MAX_PENDING_EVENTS = 100;
6147
6385
  var BRACKET_ERROR_CODE_PATTERN = /\[([a-z]+(?:[.-][a-z0-9]+)+)\]/i;
6148
6386
  var INLINE_ERROR_CODE_PATTERN = /\b([a-z]+(?:[.-][a-z0-9]+)+)\b/i;
6387
+ var MEDIA_RECORDER_ERROR_PROPERTY_KEYS = [
6388
+ "mediaRecorderErrorName",
6389
+ "mediaRecorderErrorMessage",
6390
+ "mediaRecorderMimeType",
6391
+ "mediaRecorderChunkCount",
6392
+ "mediaRecorderChunkSize"
6393
+ ];
6149
6394
  function resolveInstallationId(dependencies) {
6150
6395
  const storageProvider = dependencies.storageProvider;
6151
6396
  const stored = storageProvider?.getItem(TELEMETRY_STORAGE_KEY);
@@ -6206,7 +6451,7 @@ var TELEMETRY_EVENT_CATEGORY_MAP = {
6206
6451
  "audio.acquisition.retry": "lifecycle",
6207
6452
  "audio.acquisition.recovered": "lifecycle",
6208
6453
  "audio.acquisition.failed": "error",
6209
- "audio.warning": "error",
6454
+ "audio.warning": "performance",
6210
6455
  "recording.audio-missing": "error",
6211
6456
  "storage.init.failed": "error",
6212
6457
  "storage.write.probe.failed": "error"
@@ -6487,6 +6732,23 @@ class TelemetryClient {
6487
6732
  recordingPipelineErrorCode: recordingPipelineCode
6488
6733
  };
6489
6734
  }
6735
+ return {
6736
+ ...properties,
6737
+ ...this.buildMediaRecorderErrorProperties(error)
6738
+ };
6739
+ }
6740
+ buildMediaRecorderErrorProperties(error) {
6741
+ if (!(error && typeof error === "object")) {
6742
+ return {};
6743
+ }
6744
+ const candidate = error;
6745
+ let properties = {};
6746
+ for (const key of MEDIA_RECORDER_ERROR_PROPERTY_KEYS) {
6747
+ const value = candidate[key];
6748
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
6749
+ properties = { ...properties, [key]: value };
6750
+ }
6751
+ }
6490
6752
  return properties;
6491
6753
  }
6492
6754
  buildFingerprint() {
@@ -22557,6 +22819,9 @@ class RecordingManager {
22557
22819
  if (stopResult.encoderAcceleration !== undefined) {
22558
22820
  telemetryProperties.encoderAcceleration = stopResult.encoderAcceleration;
22559
22821
  }
22822
+ if (stopResult.mediaRecorderDiagnostics !== undefined) {
22823
+ Object.assign(telemetryProperties, stopResult.mediaRecorderDiagnostics);
22824
+ }
22560
22825
  return {
22561
22826
  blob: finalBlob,
22562
22827
  telemetryProperties
@@ -22566,6 +22831,10 @@ class RecordingManager {
22566
22831
  this.handleError(normalizedError);
22567
22832
  this.recordingState = RECORDING_STATE_IDLE;
22568
22833
  this.callbacks.onStateChange(this.recordingState);
22834
+ if (this.streamProcessor) {
22835
+ this.streamProcessor.destroy();
22836
+ this.streamProcessor = null;
22837
+ }
22569
22838
  throw normalizedError;
22570
22839
  } finally {
22571
22840
  this.activeRecordingConfig = null;
@@ -22867,6 +23136,7 @@ class RecorderController {
22867
23136
  isDestroyed = false;
22868
23137
  enableTabVisibilityOverlay = false;
22869
23138
  tabVisibilityOverlayText;
23139
+ isUploadInProgress = false;
22870
23140
  recordingWarmupTimeoutId = null;
22871
23141
  audioTelemetryUnsub = null;
22872
23142
  constructor(callbacks = {}) {
@@ -23010,6 +23280,9 @@ class RecorderController {
23010
23280
  succeededEvent: "recording.start.succeeded",
23011
23281
  failedEvent: "recording.start.failed",
23012
23282
  action: async () => {
23283
+ if (this.isUploading()) {
23284
+ throw createRecordingLifecycleError(ERROR_RECORDING_UPLOAD_IN_PROGRESS, "Previous recording is still being uploaded");
23285
+ }
23013
23286
  await this.ensureConfigReady();
23014
23287
  await this.streamManager.waitForAudio();
23015
23288
  await this.recordingManager.startRecording();
@@ -23021,6 +23294,7 @@ class RecorderController {
23021
23294
  }
23022
23295
  async stopRecording() {
23023
23296
  const sourceType = this.getCurrentSourceType();
23297
+ let shouldStopStream = true;
23024
23298
  try {
23025
23299
  const stopResult = await this.telemetryManager.executeActionWithResult({
23026
23300
  requestedEvent: "recording.stop.requested",
@@ -23031,7 +23305,6 @@ class RecorderController {
23031
23305
  await this.sourceSwitchManager.handleRecordingStop().catch(() => {
23032
23306
  throw new Error("Source switch cleanup failed");
23033
23307
  });
23034
- this.streamManager.stopStream();
23035
23308
  return recordingStopResult;
23036
23309
  },
23037
23310
  properties: {
@@ -23043,11 +23316,22 @@ class RecorderController {
23043
23316
  });
23044
23317
  return stopResult.blob;
23045
23318
  } catch (error) {
23319
+ shouldStopStream = this.shouldStopStreamAfterStopFailure(error);
23046
23320
  if (isAudioMissingError(error)) {
23047
23321
  this.sendAudioMissingTelemetry(error);
23048
23322
  }
23049
23323
  throw error;
23324
+ } finally {
23325
+ if (shouldStopStream) {
23326
+ this.streamManager.stopStream();
23327
+ }
23328
+ }
23329
+ }
23330
+ shouldStopStreamAfterStopFailure(error) {
23331
+ if (!(error && typeof error === "object" && ("code" in error))) {
23332
+ return true;
23050
23333
  }
23334
+ return error.code !== ERROR_RECORDING_STOP_NOT_READY;
23051
23335
  }
23052
23336
  sendAudioMissingTelemetry(error) {
23053
23337
  const properties = {
@@ -23063,7 +23347,7 @@ class RecorderController {
23063
23347
  properties.silenceRatio = error.details.durationMs > 0 ? error.details.mutedDurationMs / error.details.durationMs : 0;
23064
23348
  properties.hadTrackEndedWarning = error.details.hadTrackEndedWarning;
23065
23349
  }
23066
- this.telemetryManager.sendEvent("recording.audio-missing", properties);
23350
+ this.telemetryManager.sendEvent("recording.audio-missing", properties, error);
23067
23351
  }
23068
23352
  getTabVisibilityOverlayConfig() {
23069
23353
  return {
@@ -23124,8 +23408,15 @@ class RecorderController {
23124
23408
  getAudioLevel() {
23125
23409
  return this.audioLevelAnalyzer.getAudioLevel();
23126
23410
  }
23127
- async uploadVideo(blob, apiKey, backendUrl, metadata) {
23411
+ uploadVideo(blob, apiKey, backendUrl, metadata) {
23128
23412
  this.uploadCallbacks.onClearStatus();
23413
+ this.isUploadInProgress = true;
23414
+ return this.uploadVideoAfterStatusClear(blob, apiKey, backendUrl, metadata).catch((error) => {
23415
+ this.isUploadInProgress = false;
23416
+ throw error;
23417
+ });
23418
+ }
23419
+ async uploadVideoAfterStatusClear(blob, apiKey, backendUrl, metadata) {
23129
23420
  const duration = await extractVideoDuration(blob);
23130
23421
  const outputFormat = this.configManager.getConfigForRecording().format;
23131
23422
  const filename = `recording-${Date.now()}.${getFileExtensionForOutputFormat(outputFormat)}`;
@@ -23206,6 +23497,8 @@ class RecorderController {
23206
23497
  sourceType: options.sourceType
23207
23498
  }, uploadError);
23208
23499
  throw uploadError;
23500
+ } finally {
23501
+ this.isUploadInProgress = false;
23209
23502
  }
23210
23503
  }
23211
23504
  getStream() {
@@ -23229,6 +23522,7 @@ class RecorderController {
23229
23522
  }
23230
23523
  cleanup() {
23231
23524
  this.isDestroyed = true;
23525
+ this.isUploadInProgress = false;
23232
23526
  if (this.recordingWarmupTimeoutId !== null) {
23233
23527
  clearTimeout(this.recordingWarmupTimeoutId);
23234
23528
  this.recordingWarmupTimeoutId = null;
@@ -23270,6 +23564,9 @@ class RecorderController {
23270
23564
  getUploadService() {
23271
23565
  return this.uploadService;
23272
23566
  }
23567
+ isUploading() {
23568
+ return this.isUploadInProgress;
23569
+ }
23273
23570
  isRecording() {
23274
23571
  return this.streamManager.isRecording();
23275
23572
  }
@@ -23368,7 +23665,10 @@ class RecorderController {
23368
23665
  isSecureContext: profile.features.isSecureContext,
23369
23666
  hasMediaRecorder: profile.features.hasMediaRecorder,
23370
23667
  hasVideoEncoder: profile.features.hasVideoEncoder,
23371
- supportedCodecIds: profile.codecProbeResults.filter((result) => result.supported).map((result) => result.id)
23668
+ supportedCodecIds: profile.codecProbeResults.filter((result) => result.supported).map((result) => result.id),
23669
+ supportedEncodingCodecIds: profile.codecProbeResults.filter((result) => result.encodingCapability?.supported === true).map((result) => result.id),
23670
+ smoothEncodingCodecIds: profile.codecProbeResults.filter((result) => result.encodingCapability?.smooth === true).map((result) => result.id),
23671
+ powerEfficientEncodingCodecIds: profile.codecProbeResults.filter((result) => result.encodingCapability?.powerEfficient === true).map((result) => result.id)
23372
23672
  }
23373
23673
  };
23374
23674
  }
@@ -23422,9 +23722,10 @@ class RecorderController {
23422
23722
  const probeResult = this.storageManager.getWriteProbeResult();
23423
23723
  if (!probeResult?.ok) {
23424
23724
  const reason = probeResult?.reason ?? "Storage write probe did not complete";
23725
+ const storageWriteProbeError = new Error(reason);
23425
23726
  this.telemetryManager.sendEvent("storage.write.probe.failed", {
23426
23727
  reason
23427
- });
23728
+ }, storageWriteProbeError);
23428
23729
  const onStorageWriteError = resolveStorageWriteErrorCallback(this.callbacks);
23429
23730
  onStorageWriteError(reason);
23430
23731
  return;
@@ -23439,6 +23740,7 @@ class RecorderController {
23439
23740
  this.uploadCallbacks.onProgress(progress);
23440
23741
  },
23441
23742
  onUploadComplete: (id, result) => {
23743
+ this.isUploadInProgress = false;
23442
23744
  this.uploadCallbacks.onSuccess(result);
23443
23745
  const metadata = this.uploadMetadataManager.getMetadata(id);
23444
23746
  if (metadata) {
@@ -23452,6 +23754,7 @@ class RecorderController {
23452
23754
  }
23453
23755
  },
23454
23756
  onUploadError: (id, error) => {
23757
+ this.isUploadInProgress = false;
23455
23758
  this.uploadCallbacks.onError(error);
23456
23759
  const metadata = this.uploadMetadataManager.getMetadata(id);
23457
23760
  if (metadata) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidtreo/recorder",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
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",