@vidtreo/recorder 1.6.1 → 1.6.3

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 +2451 -2428
  2. package/dist/index.js +205 -69
  3. package/package.json +1 -1
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,7 @@ 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;
1410
1479
  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
1480
  function hasSufficientDeviceMemory(profile, minDeviceMemory) {
1412
1481
  const deviceMemory = profile.resources.deviceMemory;
@@ -1416,18 +1485,14 @@ function hasSufficientHardwareConcurrency(profile, minHardwareConcurrency) {
1416
1485
  const hardwareConcurrency = profile.resources.hardwareConcurrency;
1417
1486
  return hardwareConcurrency === undefined || hardwareConcurrency >= minHardwareConcurrency;
1418
1487
  }
1419
- function findSupportedTargetCodecId(profile, targetCodecIds) {
1488
+ function findSupportedTargetCodec(profile, targetCodecIds) {
1420
1489
  const targetCodecIdSet = new Set(targetCodecIds);
1421
- const supportedCodec = profile.codecProbeResults.find((result) => targetCodecIdSet.has(result.id) && result.supported);
1422
- return supportedCodec?.id;
1490
+ return profile.codecProbeResults.find((result) => targetCodecIdSet.has(result.id) && result.supported);
1423
1491
  }
1424
1492
  function canUsePostRecordingTranscodeRoute(profile, hasSupportedTargetCodec) {
1425
1493
  return profile.features.isSecureContext && profile.features.hasMediaRecorder && profile.features.hasVideoEncoder && hasSupportedTargetCodec && profile.network.online !== false;
1426
1494
  }
1427
- function isAndroidBrowser(profile) {
1428
- return profile.os.name.toLowerCase() === "android";
1429
- }
1430
- function buildLocalRouteBlockers(profile, hasSupportedTargetCodec, hasEnoughDeviceMemory, hasEnoughHardwareConcurrency) {
1495
+ function buildLocalRouteBlockers(profile, hasSupportedTargetCodec, hasEnoughDeviceMemory, hasEnoughHardwareConcurrency, hasSupportedEncodingCapability, hasSmoothEncoding, hasPowerEfficientEncoding) {
1431
1496
  const reasonCodes = [];
1432
1497
  if (!profile.features.isSecureContext) {
1433
1498
  reasonCodes.push("insecure-context");
@@ -1444,8 +1509,14 @@ function buildLocalRouteBlockers(profile, hasSupportedTargetCodec, hasEnoughDevi
1444
1509
  if (!hasEnoughHardwareConcurrency) {
1445
1510
  reasonCodes.push("insufficient-hardware-concurrency");
1446
1511
  }
1447
- if (isAndroidBrowser(profile)) {
1448
- reasonCodes.push("android-post-recording-transcode");
1512
+ if (!hasSupportedEncodingCapability) {
1513
+ reasonCodes.push("encoding-capability-unsupported");
1514
+ }
1515
+ if (!hasSmoothEncoding) {
1516
+ reasonCodes.push("encoding-not-smooth");
1517
+ }
1518
+ if (!hasPowerEfficientEncoding) {
1519
+ reasonCodes.push("encoding-not-power-efficient");
1449
1520
  }
1450
1521
  return reasonCodes;
1451
1522
  }
@@ -1459,15 +1530,31 @@ function buildFallbackRouteBlockers(profile) {
1459
1530
  }
1460
1531
  return reasonCodes;
1461
1532
  }
1533
+ function hasSupportedEncodingCapability(selectedCodec) {
1534
+ const encodingCapability = selectedCodec?.encodingCapability;
1535
+ return encodingCapability === undefined || encodingCapability.errorCode !== undefined || encodingCapability.supported === true;
1536
+ }
1537
+ function hasSmoothEncodingCapability(selectedCodec) {
1538
+ const encodingCapability = selectedCodec?.encodingCapability;
1539
+ return encodingCapability === undefined || encodingCapability.errorCode !== undefined || encodingCapability.supported !== true || encodingCapability.smooth === true;
1540
+ }
1541
+ function hasPowerEfficientEncodingCapability(selectedCodec) {
1542
+ const encodingCapability = selectedCodec?.encodingCapability;
1543
+ return encodingCapability === undefined || encodingCapability.errorCode !== undefined || encodingCapability.supported !== true || encodingCapability.powerEfficient === true;
1544
+ }
1462
1545
  function decideRecordingRoute(options) {
1463
1546
  const minDeviceMemory = options.resourceThresholds?.minDeviceMemory ?? DEFAULT_MIN_DEVICE_MEMORY_GB;
1464
1547
  const minHardwareConcurrency = options.resourceThresholds?.minHardwareConcurrency ?? DEFAULT_MIN_HARDWARE_CONCURRENCY;
1465
- const selectedCodecId = findSupportedTargetCodecId(options.profile, options.targetCodecIds);
1548
+ const selectedCodec = findSupportedTargetCodec(options.profile, options.targetCodecIds);
1549
+ const selectedCodecId = selectedCodec?.id;
1466
1550
  const hasSupportedTargetCodec = selectedCodecId !== undefined;
1467
1551
  const hasEnoughDeviceMemory = hasSufficientDeviceMemory(options.profile, minDeviceMemory);
1468
1552
  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) {
1553
+ const hasSupportedEncoding = hasSupportedEncodingCapability(selectedCodec);
1554
+ const hasSmoothEncoding = hasSmoothEncodingCapability(selectedCodec);
1555
+ const hasPowerEfficientEncoding = hasPowerEfficientEncodingCapability(selectedCodec);
1556
+ const localRouteBlockers = buildLocalRouteBlockers(options.profile, hasSupportedTargetCodec, hasEnoughDeviceMemory, hasEnoughHardwareConcurrency, hasSupportedEncoding, hasSmoothEncoding, hasPowerEfficientEncoding);
1557
+ if (options.profile.features.isSecureContext && options.profile.features.hasVideoEncoder && hasSupportedTargetCodec && options.profile.network.online !== false && hasEnoughDeviceMemory && hasEnoughHardwareConcurrency && hasSupportedEncoding && hasSmoothEncoding && hasPowerEfficientEncoding) {
1471
1558
  return {
1472
1559
  route: "local-webcodecs",
1473
1560
  reasonCodes: ["webcodecs-supported", "target-codec-supported"],
@@ -1695,6 +1782,36 @@ async function resolveAudioCodecFromPolicy(options) {
1695
1782
  return options.policy.preferredAudioCodec;
1696
1783
  }
1697
1784
 
1785
+ // src/core/transcode/output-format.ts
1786
+ function getMimeTypeForOutputFormat(format) {
1787
+ switch (format) {
1788
+ case "mp4":
1789
+ return "video/mp4";
1790
+ case "webm":
1791
+ return "video/webm";
1792
+ case "mkv":
1793
+ return "video/x-matroska";
1794
+ case "mov":
1795
+ return "video/quicktime";
1796
+ default:
1797
+ throw new Error(`Unsupported output format: ${format}`);
1798
+ }
1799
+ }
1800
+ function getFileExtensionForOutputFormat(format) {
1801
+ switch (format) {
1802
+ case "mp4":
1803
+ return "mp4";
1804
+ case "webm":
1805
+ return "webm";
1806
+ case "mkv":
1807
+ return "mkv";
1808
+ case "mov":
1809
+ return "mov";
1810
+ default:
1811
+ throw new Error(`Unsupported output format: ${format}`);
1812
+ }
1813
+ }
1814
+
1698
1815
  // src/core/transcode/video-transcoder.ts
1699
1816
  var ALLOW_ROTATION_METADATA = false;
1700
1817
  function createSource(input) {
@@ -1720,20 +1837,6 @@ function createOutputFormat(format) {
1720
1837
  throw new Error(`Unsupported output format: ${format}`);
1721
1838
  }
1722
1839
  }
1723
- function getMimeTypeForFormat(format) {
1724
- switch (format) {
1725
- case "mp4":
1726
- return "video/mp4";
1727
- case "webm":
1728
- return "video/webm";
1729
- case "mkv":
1730
- return "video/x-matroska";
1731
- case "mov":
1732
- return "video/quicktime";
1733
- default:
1734
- throw new Error(`Unsupported output format: ${format}`);
1735
- }
1736
- }
1737
1840
  function createConversionOptions(config, optimizeForSpeed = false) {
1738
1841
  const video = {
1739
1842
  fit: "contain",
@@ -1828,7 +1931,7 @@ async function transcodeVideo(input, config = {}, onProgress) {
1828
1931
  if (!buffer) {
1829
1932
  throw new Error("Transcoding completed but no output buffer was generated");
1830
1933
  }
1831
- const mimeType = getMimeTypeForFormat(finalConfig.format);
1934
+ const mimeType = getMimeTypeForOutputFormat(finalConfig.format);
1832
1935
  return {
1833
1936
  buffer,
1834
1937
  blob: new Blob([buffer], { type: mimeType })
@@ -1899,7 +2002,7 @@ async function transcodeVideoForNativeCamera(file, config = {}, onProgress) {
1899
2002
  if (!buffer) {
1900
2003
  throw new Error("Transcoding completed but no output buffer was generated");
1901
2004
  }
1902
- const mimeType = getMimeTypeForFormat(finalConfig.format);
2005
+ const mimeType = getMimeTypeForOutputFormat(finalConfig.format);
1903
2006
  return {
1904
2007
  buffer,
1905
2008
  blob: new Blob([buffer], { type: mimeType })
@@ -4936,6 +5039,7 @@ class StreamManager {
4936
5039
  var ERROR_RECORDING_STOP_NOT_READY = "recording.stop-not-ready";
4937
5040
  var ERROR_RECORDING_FINALIZE_NOT_READY = "recording.finalize-not-ready";
4938
5041
  var ERROR_RECORDING_FINALIZE_TIMEOUT = "recording.finalize-timeout";
5042
+ var ERROR_RECORDING_UPLOAD_IN_PROGRESS = "recording.upload-in-progress";
4939
5043
  function createRecordingLifecycleError(code, message, details) {
4940
5044
  const error = new Error(`${message} [${code}]`);
4941
5045
  error.code = code;
@@ -4962,7 +5066,7 @@ function normalizeRecordingLifecycleError(error) {
4962
5066
  }
4963
5067
  function isRecordingLifecycleError(error) {
4964
5068
  const errorWithCode = error;
4965
- return errorWithCode.code === ERROR_RECORDING_STOP_NOT_READY || errorWithCode.code === ERROR_RECORDING_FINALIZE_NOT_READY || errorWithCode.code === ERROR_RECORDING_FINALIZE_TIMEOUT;
5069
+ 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;
4966
5070
  }
4967
5071
 
4968
5072
  // src/core/utils/formatters.ts
@@ -5142,11 +5246,6 @@ var LOW_RESOURCE_SAFE_CAPTURE_CONSTRAINTS = {
5142
5246
  height: { ideal: 480, max: 480 },
5143
5247
  frameRate: { ideal: 20, max: 20 }
5144
5248
  };
5145
- var ANDROID_SAFE_CAPTURE_CONSTRAINTS = {
5146
- width: { ideal: 1280, max: 1280 },
5147
- height: { ideal: 720, max: 720 },
5148
- frameRate: { ideal: 24, max: 24 }
5149
- };
5150
5249
  function createUnsupportedRouteError(routeDecision) {
5151
5250
  const guidance = routeDecision.guidance?.message ?? "This browser or device cannot safely record video. Try a supported browser or upload an existing video file.";
5152
5251
  const error = new Error(guidance);
@@ -5533,12 +5632,9 @@ class StreamRecordingState {
5533
5632
  }
5534
5633
  resolveSafeCaptureVideoConstraints() {
5535
5634
  const reasonCodes = this.activeRouteDecision.reasonCodes;
5536
- if (reasonCodes.includes("insufficient-device-memory") || reasonCodes.includes("insufficient-hardware-concurrency")) {
5635
+ 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")) {
5537
5636
  return LOW_RESOURCE_SAFE_CAPTURE_CONSTRAINTS;
5538
5637
  }
5539
- if (reasonCodes.includes("android-post-recording-transcode")) {
5540
- return ANDROID_SAFE_CAPTURE_CONSTRAINTS;
5541
- }
5542
5638
  return null;
5543
5639
  }
5544
5640
  async stopSafeCaptureRecording() {
@@ -6045,7 +6141,7 @@ function resolveDeviceError(input) {
6045
6141
  // package.json
6046
6142
  var package_default = {
6047
6143
  name: "@vidtreo/recorder",
6048
- version: "1.6.1",
6144
+ version: "1.6.3",
6049
6145
  type: "module",
6050
6146
  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.",
6051
6147
  main: "./dist/index.js",
@@ -6190,7 +6286,7 @@ var TELEMETRY_EVENT_CATEGORY_MAP = {
6190
6286
  "audio.acquisition.retry": "lifecycle",
6191
6287
  "audio.acquisition.recovered": "lifecycle",
6192
6288
  "audio.acquisition.failed": "error",
6193
- "audio.warning": "error",
6289
+ "audio.warning": "performance",
6194
6290
  "recording.audio-missing": "error",
6195
6291
  "storage.init.failed": "error",
6196
6292
  "storage.write.probe.failed": "error"
@@ -6852,6 +6948,7 @@ class UploadQueueManager {
6852
6948
  apiKey: upload.apiKey,
6853
6949
  backendUrl: upload.backendUrl,
6854
6950
  filename: upload.filename,
6951
+ mimeType: upload.mimeType,
6855
6952
  metadata: upload.metadata,
6856
6953
  userMetadata: upload.userMetadata,
6857
6954
  onProgress: (progress) => {
@@ -6899,6 +6996,7 @@ class UploadQueueManager {
6899
6996
 
6900
6997
  // src/core/upload/upload-service.ts
6901
6998
  var API_UPLOAD_PATH = "/api/v1/videos/upload";
6999
+ var MIME_TYPE_PARAMETER_SEPARATOR = ";";
6902
7000
 
6903
7001
  class VideoUploadService {
6904
7002
  uploadVideo(blob, options) {
@@ -6913,6 +7011,7 @@ class VideoUploadService {
6913
7011
  uploadVideoFile(blob, options) {
6914
7012
  return new Promise((resolve, reject) => {
6915
7013
  const xhr = new XMLHttpRequest;
7014
+ const uploadBlob = this.createUploadBlob(blob, options.mimeType);
6916
7015
  if (options.onProgress) {
6917
7016
  const onProgress = options.onProgress;
6918
7017
  xhr.upload.addEventListener("progress", (event) => {
@@ -6939,10 +7038,10 @@ class VideoUploadService {
6939
7038
  xhr.open("POST", url);
6940
7039
  xhr.setRequestHeader("Authorization", `Bearer ${options.apiKey}`);
6941
7040
  const formData = new FormData;
6942
- formData.append("file", blob, options.filename);
7041
+ formData.append("file", uploadBlob, options.filename);
6943
7042
  const metadata = {
6944
7043
  filename: options.filename,
6945
- mimeType: blob.type
7044
+ mimeType: uploadBlob.type
6946
7045
  };
6947
7046
  if (options.userMetadata) {
6948
7047
  metadata.userMetadata = options.userMetadata;
@@ -6951,6 +7050,26 @@ class VideoUploadService {
6951
7050
  xhr.send(formData);
6952
7051
  });
6953
7052
  }
7053
+ createUploadBlob(blob, mimeType) {
7054
+ const normalizedMimeType = this.resolveUploadMimeType(blob, mimeType);
7055
+ if (normalizedMimeType === blob.type) {
7056
+ return blob;
7057
+ }
7058
+ return new Blob([blob], { type: normalizedMimeType });
7059
+ }
7060
+ resolveUploadMimeType(blob, mimeType) {
7061
+ if (mimeType && mimeType.trim().length > 0) {
7062
+ return this.normalizeMimeType(mimeType);
7063
+ }
7064
+ return this.normalizeMimeType(blob.type);
7065
+ }
7066
+ normalizeMimeType(mimeType) {
7067
+ const [baseMimeType] = mimeType.split(MIME_TYPE_PARAMETER_SEPARATOR);
7068
+ if (baseMimeType === undefined) {
7069
+ return mimeType.trim().toLowerCase();
7070
+ }
7071
+ return baseMimeType.trim().toLowerCase();
7072
+ }
6954
7073
  parseSuccessResponse(xhr, resolve, reject) {
6955
7074
  const parsed = this.safeParseJsonFromXhr(xhr);
6956
7075
  if (!parsed) {
@@ -22276,23 +22395,9 @@ var DEFAULT_ROUTE_DECISION2 = {
22276
22395
  var DEFAULT_RECORDING_MANAGER_DEPENDENCIES = {
22277
22396
  transcodeVideo
22278
22397
  };
22279
- function getExpectedMimeTypePrefix(config) {
22280
- switch (config.format) {
22281
- case "mp4":
22282
- return "video/mp4";
22283
- case "webm":
22284
- return "video/webm";
22285
- case "mkv":
22286
- return "video/x-matroska";
22287
- case "mov":
22288
- return "video/quicktime";
22289
- default:
22290
- return null;
22291
- }
22292
- }
22293
22398
  function isBlobAlreadyInTargetFormat(blob, config) {
22294
- const expectedMimeTypePrefix = getExpectedMimeTypePrefix(config);
22295
- return expectedMimeTypePrefix !== null && blob.type.toLowerCase().startsWith(expectedMimeTypePrefix);
22399
+ const expectedMimeTypePrefix = getMimeTypeForOutputFormat(config.format);
22400
+ return blob.type.toLowerCase().startsWith(expectedMimeTypePrefix);
22296
22401
  }
22297
22402
 
22298
22403
  class RecordingManager {
@@ -22842,6 +22947,7 @@ class RecorderController {
22842
22947
  isDestroyed = false;
22843
22948
  enableTabVisibilityOverlay = false;
22844
22949
  tabVisibilityOverlayText;
22950
+ isUploadInProgress = false;
22845
22951
  recordingWarmupTimeoutId = null;
22846
22952
  audioTelemetryUnsub = null;
22847
22953
  constructor(callbacks = {}) {
@@ -22985,6 +23091,9 @@ class RecorderController {
22985
23091
  succeededEvent: "recording.start.succeeded",
22986
23092
  failedEvent: "recording.start.failed",
22987
23093
  action: async () => {
23094
+ if (this.isUploading()) {
23095
+ throw createRecordingLifecycleError(ERROR_RECORDING_UPLOAD_IN_PROGRESS, "Previous recording is still being uploaded");
23096
+ }
22988
23097
  await this.ensureConfigReady();
22989
23098
  await this.streamManager.waitForAudio();
22990
23099
  await this.recordingManager.startRecording();
@@ -23038,7 +23147,7 @@ class RecorderController {
23038
23147
  properties.silenceRatio = error.details.durationMs > 0 ? error.details.mutedDurationMs / error.details.durationMs : 0;
23039
23148
  properties.hadTrackEndedWarning = error.details.hadTrackEndedWarning;
23040
23149
  }
23041
- this.telemetryManager.sendEvent("recording.audio-missing", properties);
23150
+ this.telemetryManager.sendEvent("recording.audio-missing", properties, error);
23042
23151
  }
23043
23152
  getTabVisibilityOverlayConfig() {
23044
23153
  return {
@@ -23099,10 +23208,19 @@ class RecorderController {
23099
23208
  getAudioLevel() {
23100
23209
  return this.audioLevelAnalyzer.getAudioLevel();
23101
23210
  }
23102
- async uploadVideo(blob, apiKey, backendUrl, metadata) {
23211
+ uploadVideo(blob, apiKey, backendUrl, metadata) {
23103
23212
  this.uploadCallbacks.onClearStatus();
23213
+ this.isUploadInProgress = true;
23214
+ return this.uploadVideoAfterStatusClear(blob, apiKey, backendUrl, metadata).catch((error) => {
23215
+ this.isUploadInProgress = false;
23216
+ throw error;
23217
+ });
23218
+ }
23219
+ async uploadVideoAfterStatusClear(blob, apiKey, backendUrl, metadata) {
23104
23220
  const duration = await extractVideoDuration(blob);
23105
- const filename = `recording-${Date.now()}.mp4`;
23221
+ const outputFormat = this.configManager.getConfigForRecording().format;
23222
+ const filename = `recording-${Date.now()}.${getFileExtensionForOutputFormat(outputFormat)}`;
23223
+ const mimeType = getMimeTypeForOutputFormat(outputFormat);
23106
23224
  const sourceType = this.getCurrentSourceType();
23107
23225
  let userMetadata;
23108
23226
  if (Object.keys(metadata).length > 0) {
@@ -23119,6 +23237,7 @@ class RecorderController {
23119
23237
  apiKey,
23120
23238
  backendUrl,
23121
23239
  filename,
23240
+ mimeType,
23122
23241
  duration,
23123
23242
  sourceType,
23124
23243
  userMetadata
@@ -23130,6 +23249,7 @@ class RecorderController {
23130
23249
  apiKey,
23131
23250
  backendUrl,
23132
23251
  filename,
23252
+ mimeType,
23133
23253
  duration,
23134
23254
  metadata: undefined,
23135
23255
  userMetadata
@@ -23156,6 +23276,7 @@ class RecorderController {
23156
23276
  apiKey: options.apiKey,
23157
23277
  backendUrl: options.backendUrl,
23158
23278
  filename: options.filename,
23279
+ mimeType: options.mimeType,
23159
23280
  metadata: undefined,
23160
23281
  userMetadata: options.userMetadata,
23161
23282
  onProgress: this.uploadCallbacks.onProgress
@@ -23176,6 +23297,8 @@ class RecorderController {
23176
23297
  sourceType: options.sourceType
23177
23298
  }, uploadError);
23178
23299
  throw uploadError;
23300
+ } finally {
23301
+ this.isUploadInProgress = false;
23179
23302
  }
23180
23303
  }
23181
23304
  getStream() {
@@ -23199,6 +23322,7 @@ class RecorderController {
23199
23322
  }
23200
23323
  cleanup() {
23201
23324
  this.isDestroyed = true;
23325
+ this.isUploadInProgress = false;
23202
23326
  if (this.recordingWarmupTimeoutId !== null) {
23203
23327
  clearTimeout(this.recordingWarmupTimeoutId);
23204
23328
  this.recordingWarmupTimeoutId = null;
@@ -23240,6 +23364,9 @@ class RecorderController {
23240
23364
  getUploadService() {
23241
23365
  return this.uploadService;
23242
23366
  }
23367
+ isUploading() {
23368
+ return this.isUploadInProgress;
23369
+ }
23243
23370
  isRecording() {
23244
23371
  return this.streamManager.isRecording();
23245
23372
  }
@@ -23338,7 +23465,10 @@ class RecorderController {
23338
23465
  isSecureContext: profile.features.isSecureContext,
23339
23466
  hasMediaRecorder: profile.features.hasMediaRecorder,
23340
23467
  hasVideoEncoder: profile.features.hasVideoEncoder,
23341
- supportedCodecIds: profile.codecProbeResults.filter((result) => result.supported).map((result) => result.id)
23468
+ supportedCodecIds: profile.codecProbeResults.filter((result) => result.supported).map((result) => result.id),
23469
+ supportedEncodingCodecIds: profile.codecProbeResults.filter((result) => result.encodingCapability?.supported === true).map((result) => result.id),
23470
+ smoothEncodingCodecIds: profile.codecProbeResults.filter((result) => result.encodingCapability?.smooth === true).map((result) => result.id),
23471
+ powerEfficientEncodingCodecIds: profile.codecProbeResults.filter((result) => result.encodingCapability?.powerEfficient === true).map((result) => result.id)
23342
23472
  }
23343
23473
  };
23344
23474
  }
@@ -23392,9 +23522,10 @@ class RecorderController {
23392
23522
  const probeResult = this.storageManager.getWriteProbeResult();
23393
23523
  if (!probeResult?.ok) {
23394
23524
  const reason = probeResult?.reason ?? "Storage write probe did not complete";
23525
+ const storageWriteProbeError = new Error(reason);
23395
23526
  this.telemetryManager.sendEvent("storage.write.probe.failed", {
23396
23527
  reason
23397
- });
23528
+ }, storageWriteProbeError);
23398
23529
  const onStorageWriteError = resolveStorageWriteErrorCallback(this.callbacks);
23399
23530
  onStorageWriteError(reason);
23400
23531
  return;
@@ -23409,6 +23540,7 @@ class RecorderController {
23409
23540
  this.uploadCallbacks.onProgress(progress);
23410
23541
  },
23411
23542
  onUploadComplete: (id, result) => {
23543
+ this.isUploadInProgress = false;
23412
23544
  this.uploadCallbacks.onSuccess(result);
23413
23545
  const metadata = this.uploadMetadataManager.getMetadata(id);
23414
23546
  if (metadata) {
@@ -23422,6 +23554,7 @@ class RecorderController {
23422
23554
  }
23423
23555
  },
23424
23556
  onUploadError: (id, error) => {
23557
+ this.isUploadInProgress = false;
23425
23558
  this.uploadCallbacks.onError(error);
23426
23559
  const metadata = this.uploadMetadataManager.getMetadata(id);
23427
23560
  if (metadata) {
@@ -23854,8 +23987,10 @@ class VidtreoRecorder {
23854
23987
  async stopRecording() {
23855
23988
  await this.ensureInitialized();
23856
23989
  const blob = await this.controller.stopRecording();
23990
+ const outputFormat = (await this.controller.getConfig()).format;
23857
23991
  const timestamp = Date.now();
23858
- const filename = `recording-${timestamp}.mp4`;
23992
+ const filename = `recording-${timestamp}.${getFileExtensionForOutputFormat(outputFormat)}`;
23993
+ const mimeType = getMimeTypeForOutputFormat(outputFormat);
23859
23994
  const uploadService = this.controller.getUploadService();
23860
23995
  if (!uploadService) {
23861
23996
  throw new Error("Upload service not available");
@@ -23864,6 +23999,7 @@ class VidtreoRecorder {
23864
23999
  apiKey: this.config.apiKey,
23865
24000
  backendUrl: this.config.apiUrl,
23866
24001
  filename,
24002
+ mimeType,
23867
24003
  userMetadata: this.config.userMetadata,
23868
24004
  onProgress: this.config.onUploadProgress
23869
24005
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidtreo/recorder",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
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",