@vidtreo/recorder 1.3.4 → 1.4.1

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 +2025 -1713
  2. package/dist/index.js +1143 -127
  3. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -224,7 +224,9 @@ var HD_SIZE_LIMIT_MB_PER_MINUTE = 8;
224
224
  var FHD_SIZE_LIMIT_MB_PER_MINUTE = 18;
225
225
  var K4_SIZE_LIMIT_MB_PER_MINUTE = 46;
226
226
  var MP4_AUDIO_BITRATE = 128000;
227
+ var MP4_AUDIO_BITRATE_SD = 64000;
227
228
  var WEBM_AUDIO_BITRATE = 96000;
229
+ var WEBM_AUDIO_BITRATE_SD = 48000;
228
230
  var VIDEO_CODEC_AVC = "avc";
229
231
  var VIDEO_CODEC_VP9 = "vp9";
230
232
  var VIDEO_CODEC_AV1 = "av1";
@@ -323,13 +325,19 @@ function getPresetTotalBitrate(preset) {
323
325
  const sizeLimit = PRESET_SIZE_LIMIT_MB_PER_MINUTE[preset];
324
326
  return calculateTotalBitrateFromMbPerMinute(sizeLimit);
325
327
  }
326
- function getPresetAudioBitrateForFormat(format) {
328
+ function getPresetAudioBitrateForFormat(format, preset) {
329
+ if (preset === "sd") {
330
+ if (format === "webm") {
331
+ return WEBM_AUDIO_BITRATE_SD;
332
+ }
333
+ return MP4_AUDIO_BITRATE_SD;
334
+ }
327
335
  const policy = getFormatCompatibilityPolicy(format);
328
336
  return policy.audioBitrate;
329
337
  }
330
338
  function getPresetVideoBitrateForFormat(preset, format) {
331
339
  const totalBitrate = getPresetTotalBitrate(preset);
332
- const audioBitrate = getPresetAudioBitrateForFormat(format);
340
+ const audioBitrate = getPresetAudioBitrateForFormat(format, preset);
333
341
  return calculateVideoBitrate(totalBitrate, audioBitrate);
334
342
  }
335
343
  var PRESET_VIDEO_BITRATE_MAP = {
@@ -350,6 +358,7 @@ var MOBILE_RESOLUTION_MAP = {
350
358
  fhd: { width: 1080, height: 1920 },
351
359
  "4k": { width: 2160, height: 3840 }
352
360
  };
361
+ var PRESET_DEFAULT_FPS = 30;
353
362
  var DEFAULT_BACKEND_URL = "https://core.vidtreo.com";
354
363
  var DEFAULT_TRANSCODE_CONFIG = Object.freeze({
355
364
  format: "mp4",
@@ -378,7 +387,8 @@ function getDefaultConfigForFormat(format) {
378
387
  // src/core/utils/device-detector.ts
379
388
  import { UAParser as UAParser2 } from "ua-parser-js";
380
389
  function isMobileDevice() {
381
- const parser = new UAParser2;
390
+ const userAgent = globalThis.navigator && typeof globalThis.navigator.userAgent === "string" ? globalThis.navigator.userAgent : "";
391
+ const parser = new UAParser2(userAgent);
382
392
  const device = parser.getDevice();
383
393
  if (device.type === "mobile") {
384
394
  return true;
@@ -485,11 +495,13 @@ function mapPresetToConfig(options) {
485
495
  });
486
496
  const config = {
487
497
  format,
498
+ fps: PRESET_DEFAULT_FPS,
488
499
  width,
489
500
  height,
490
501
  bitrate: getPresetVideoBitrateForFormat(preset, format),
491
502
  audioCodec: policy.preferredAudioCodec,
492
- audioBitrate: policy.audioBitrate
503
+ audioBitrate: getPresetAudioBitrateForFormat(format, preset),
504
+ latencyMode: preset === "sd" ? "quality" : undefined
493
505
  };
494
506
  if (watermark) {
495
507
  config.watermark = {
@@ -704,6 +716,488 @@ class DeviceManager {
704
716
  return this.availableDevices;
705
717
  }
706
718
  }
719
+ // src/core/device/permission-checker.ts
720
+ var noOperation = () => {
721
+ return;
722
+ };
723
+ function isPermissionsApiAvailable() {
724
+ if (typeof navigator === "undefined") {
725
+ return false;
726
+ }
727
+ if (!navigator.permissions) {
728
+ return false;
729
+ }
730
+ return typeof navigator.permissions.query === "function";
731
+ }
732
+ function queryPermissionState(permissionName) {
733
+ if (!isPermissionsApiAvailable()) {
734
+ return Promise.resolve("unknown");
735
+ }
736
+ return navigator.permissions.query({ name: permissionName }).then((status) => status.state).catch(() => "unknown");
737
+ }
738
+ async function queryAllPermissions() {
739
+ const [camera, microphone] = await Promise.all([
740
+ queryPermissionState("camera"),
741
+ queryPermissionState("microphone")
742
+ ]);
743
+ return { camera, microphone };
744
+ }
745
+ function createPermissionChangeListener(permissionName, onStateChange) {
746
+ if (!isPermissionsApiAvailable()) {
747
+ return noOperation;
748
+ }
749
+ let permissionStatus = null;
750
+ let isDestroyed = false;
751
+ const handleChange = () => {
752
+ if (!permissionStatus) {
753
+ return;
754
+ }
755
+ onStateChange(permissionStatus.state);
756
+ };
757
+ navigator.permissions.query({ name: permissionName }).then((status) => {
758
+ if (isDestroyed) {
759
+ return;
760
+ }
761
+ permissionStatus = status;
762
+ permissionStatus.addEventListener("change", handleChange);
763
+ }).catch(noOperation);
764
+ return () => {
765
+ isDestroyed = true;
766
+ if (permissionStatus) {
767
+ permissionStatus.removeEventListener("change", handleChange);
768
+ permissionStatus = null;
769
+ }
770
+ };
771
+ }
772
+
773
+ // src/core/device/permission-recovery.ts
774
+ var CHROME_INSTRUCTIONS = {
775
+ en: "Click the lock/tune icon in the address bar → Site settings → Camera/Microphone → Allow",
776
+ es: "Haz clic en el icono de candado/ajustes en la barra de direcciones → Configuración del sitio → Cámara/Micrófono → Permitir",
777
+ resetUrl: "chrome://settings/content/camera"
778
+ };
779
+ var SAFARI_INSTRUCTIONS = {
780
+ en: "Go to Safari → Settings → Websites → Camera/Microphone → Allow for this site",
781
+ es: "Ve a Safari → Ajustes → Sitios web → Cámara/Micrófono → Permitir para este sitio"
782
+ };
783
+ var EDGE_INSTRUCTIONS = {
784
+ en: "Click the lock icon in the address bar → Permissions → Camera/Microphone → Allow",
785
+ es: "Haz clic en el icono de candado en la barra de direcciones → Permisos → Cámara/Micrófono → Permitir",
786
+ resetUrl: "edge://settings/content/camera"
787
+ };
788
+ var OPERA_INSTRUCTIONS = {
789
+ en: "Click the lock icon → Site settings → Camera/Microphone → Allow",
790
+ es: "Haz clic en el icono de candado → Configuración del sitio → Cámara/Micrófono → Permitir",
791
+ resetUrl: "opera://settings/content/camera"
792
+ };
793
+ var UNKNOWN_BROWSER_INSTRUCTIONS = {
794
+ en: "Please check your browser settings to allow camera and microphone access for this site",
795
+ es: "Por favor, revisa la configuración de tu navegador para permitir el acceso a la cámara y el micrófono en este sitio"
796
+ };
797
+ var INSECURE_CONTEXT_INSTRUCTIONS = {
798
+ en: "This site must be accessed over HTTPS (secure connection) to use camera and microphone",
799
+ es: "Este sitio debe ser accedido a través de HTTPS (conexión segura) para usar cámara y micrófono"
800
+ };
801
+ var BROWSER_INSTRUCTIONS_MAP = {
802
+ chrome: CHROME_INSTRUCTIONS,
803
+ safari: SAFARI_INSTRUCTIONS,
804
+ edge: EDGE_INSTRUCTIONS,
805
+ opera: OPERA_INSTRUCTIONS,
806
+ unknown: UNKNOWN_BROWSER_INSTRUCTIONS
807
+ };
808
+ var RETRYABLE_ERROR_CODES = new Set([
809
+ "permission-denied-camera",
810
+ "permission-denied-microphone",
811
+ "permission-denied-both",
812
+ "device-not-found",
813
+ "device-in-use"
814
+ ]);
815
+ function resolveBrowserKey(browserName) {
816
+ const normalizedName = browserName.toLowerCase();
817
+ if (normalizedName.includes("edge")) {
818
+ return "edge";
819
+ }
820
+ if (normalizedName.includes("opera") || normalizedName.includes("opr")) {
821
+ return "opera";
822
+ }
823
+ if (normalizedName.includes("chrome")) {
824
+ return "chrome";
825
+ }
826
+ if (normalizedName.includes("safari")) {
827
+ return "safari";
828
+ }
829
+ return "unknown";
830
+ }
831
+ function createPermissionRecoveryData(errorCode, browserInfo, language = "en") {
832
+ if (errorCode === "insecure-context") {
833
+ return {
834
+ errorCode,
835
+ browserName: browserInfo.name,
836
+ browserVersion: browserInfo.version,
837
+ resetInstructions: INSECURE_CONTEXT_INSTRUCTIONS[language],
838
+ resetUrl: undefined,
839
+ canRetry: false
840
+ };
841
+ }
842
+ const browserKey = resolveBrowserKey(browserInfo.name);
843
+ const instructions = BROWSER_INSTRUCTIONS_MAP[browserKey];
844
+ const canRetry = RETRYABLE_ERROR_CODES.has(errorCode);
845
+ return {
846
+ errorCode,
847
+ browserName: browserInfo.name,
848
+ browserVersion: browserInfo.version,
849
+ resetInstructions: instructions[language],
850
+ resetUrl: instructions.resetUrl,
851
+ canRetry
852
+ };
853
+ }
854
+
855
+ // src/core/device/permission-flow-orchestrator.ts
856
+ var GRANTED_PERMISSION_COUNT = 2;
857
+ var DEFAULT_PERMISSION_STATUS = {
858
+ camera: "unknown",
859
+ microphone: "unknown"
860
+ };
861
+ function noOperation2() {
862
+ return;
863
+ }
864
+ function getIsSecureContext() {
865
+ const globalWindow = globalThis.window;
866
+ if (!globalWindow) {
867
+ return false;
868
+ }
869
+ return globalWindow.isSecureContext;
870
+ }
871
+ function stopStreamTracks(stream) {
872
+ const tracks = stream.getTracks();
873
+ for (const track of tracks) {
874
+ track.stop();
875
+ }
876
+ }
877
+ async function requestPermissionProbe() {
878
+ const mediaDevices = globalThis.navigator?.mediaDevices;
879
+ if (!mediaDevices) {
880
+ return;
881
+ }
882
+ const canGetUserMedia = typeof mediaDevices.getUserMedia === "function";
883
+ if (!canGetUserMedia) {
884
+ return;
885
+ }
886
+ const stream = await mediaDevices.getUserMedia({ audio: true, video: true });
887
+ stopStreamTracks(stream);
888
+ await mediaDevices.getUserMedia({
889
+ audio: true,
890
+ video: { width: { ideal: 1920 }, height: { ideal: 1080 } }
891
+ }).then((stream2) => {
892
+ stopStreamTracks(stream2);
893
+ return;
894
+ }).catch(noOperation2);
895
+ }
896
+ function countGrantedPermissions(permissions) {
897
+ let grantedCount = 0;
898
+ if (permissions.camera === "granted") {
899
+ grantedCount += 1;
900
+ }
901
+ if (permissions.microphone === "granted") {
902
+ grantedCount += 1;
903
+ }
904
+ return grantedCount;
905
+ }
906
+ function hasUnknownPermissionState(permissions) {
907
+ if (permissions.camera === "unknown") {
908
+ return true;
909
+ }
910
+ if (permissions.microphone === "unknown") {
911
+ return true;
912
+ }
913
+ return false;
914
+ }
915
+ function hasPromptPermissionState(permissions) {
916
+ if (permissions.camera === "prompt") {
917
+ return true;
918
+ }
919
+ if (permissions.microphone === "prompt") {
920
+ return true;
921
+ }
922
+ return false;
923
+ }
924
+ function hasHardPermissionDenial(permissions) {
925
+ if (permissions.camera === "denied") {
926
+ return true;
927
+ }
928
+ if (permissions.microphone === "denied") {
929
+ return true;
930
+ }
931
+ return false;
932
+ }
933
+ function getPermissionDeniedIssueCode(permissions) {
934
+ const hasCameraDenied = permissions.camera === "denied";
935
+ const hasMicrophoneDenied = permissions.microphone === "denied";
936
+ if (hasCameraDenied && hasMicrophoneDenied) {
937
+ return "permission-denied-both";
938
+ }
939
+ if (hasCameraDenied) {
940
+ return "permission-denied-camera";
941
+ }
942
+ return "permission-denied-microphone";
943
+ }
944
+ function createPermissionFlowOrchestratorDependencies() {
945
+ return {
946
+ queryAllPermissions,
947
+ createPermissionChangeListener,
948
+ createPermissionRecoveryData,
949
+ getBrowserInfo: () => {
950
+ const browserInfo = getBrowserInfo();
951
+ return {
952
+ name: browserInfo.name,
953
+ version: browserInfo.version
954
+ };
955
+ },
956
+ getIsSecureContext,
957
+ requestPermissionProbe
958
+ };
959
+ }
960
+
961
+ class PermissionFlowOrchestrator {
962
+ dependencies;
963
+ callbacks;
964
+ language;
965
+ currentState;
966
+ cleanupCameraListener = noOperation2;
967
+ cleanupMicrophoneListener = noOperation2;
968
+ isInitialized = false;
969
+ hasCompleted = false;
970
+ constructor(dependencies, options = {}) {
971
+ this.dependencies = dependencies;
972
+ let callbacks = {};
973
+ if (options.callbacks) {
974
+ callbacks = options.callbacks;
975
+ }
976
+ this.callbacks = callbacks;
977
+ let language = "en";
978
+ if (options.language === "es") {
979
+ language = "es";
980
+ }
981
+ this.language = language;
982
+ this.currentState = {
983
+ step: "idle",
984
+ permissions: DEFAULT_PERMISSION_STATUS,
985
+ denialType: "none",
986
+ isSecureContext: true,
987
+ isComplete: false,
988
+ canRetry: true,
989
+ shouldProbeUnknown: true
990
+ };
991
+ }
992
+ initialize() {
993
+ if (this.isInitialized) {
994
+ return Promise.resolve(this.currentState);
995
+ }
996
+ this.isInitialized = true;
997
+ this.hasCompleted = false;
998
+ this.setupPermissionListeners();
999
+ this.updateStateForChecking();
1000
+ this.emitChange("initialized");
1001
+ const isSecureContext = this.dependencies.getIsSecureContext();
1002
+ if (!isSecureContext) {
1003
+ this.currentState = this.createInsecureContextState();
1004
+ this.emitChange("insecure-context");
1005
+ this.tryComplete();
1006
+ return Promise.resolve(this.currentState);
1007
+ }
1008
+ return this.refreshPermissions("permission-refresh");
1009
+ }
1010
+ requestCurrentStep() {
1011
+ if (!this.isInitialized) {
1012
+ return this.initialize();
1013
+ }
1014
+ if (!this.currentState.canRetry) {
1015
+ return Promise.resolve(this.currentState);
1016
+ }
1017
+ this.updateStateForChecking();
1018
+ this.emitChange("current-step-requested");
1019
+ return this.requestPermissionAndRefresh();
1020
+ }
1021
+ retryCurrentStep() {
1022
+ if (!this.isInitialized) {
1023
+ return this.initialize();
1024
+ }
1025
+ if (!this.currentState.canRetry) {
1026
+ return Promise.resolve(this.currentState);
1027
+ }
1028
+ this.updateStateForChecking();
1029
+ this.emitChange("retry-requested");
1030
+ return this.requestPermissionAndRefresh();
1031
+ }
1032
+ getState() {
1033
+ return this.currentState;
1034
+ }
1035
+ requestPermissionAndRefresh() {
1036
+ return this.dependencies.requestPermissionProbe().then(() => {
1037
+ const grantedPermissions = {
1038
+ camera: "granted",
1039
+ microphone: "granted"
1040
+ };
1041
+ this.currentState = this.createStateFromPermissions(grantedPermissions);
1042
+ this.emitChange("probe-complete");
1043
+ this.tryComplete();
1044
+ return this.currentState;
1045
+ }).catch(() => this.refreshPermissions("permission-refresh").then((state) => {
1046
+ if (state.step !== "awaiting-user") {
1047
+ return state;
1048
+ }
1049
+ return this.transitionToBlockedFromFailedProbe();
1050
+ }));
1051
+ }
1052
+ destroy() {
1053
+ this.cleanupCameraListener();
1054
+ this.cleanupCameraListener = noOperation2;
1055
+ this.cleanupMicrophoneListener();
1056
+ this.cleanupMicrophoneListener = noOperation2;
1057
+ this.isInitialized = false;
1058
+ this.hasCompleted = false;
1059
+ this.emitChange("destroyed");
1060
+ }
1061
+ setupPermissionListeners() {
1062
+ this.cleanupCameraListener = this.dependencies.createPermissionChangeListener("camera", (state) => {
1063
+ this.handlePermissionStateChange("camera", state);
1064
+ });
1065
+ this.cleanupMicrophoneListener = this.dependencies.createPermissionChangeListener("microphone", (state) => {
1066
+ this.handlePermissionStateChange("microphone", state);
1067
+ });
1068
+ }
1069
+ handlePermissionStateChange(permissionName, state) {
1070
+ if (!this.isInitialized) {
1071
+ return;
1072
+ }
1073
+ const nextPermissions = {
1074
+ ...this.currentState.permissions,
1075
+ [permissionName]: state
1076
+ };
1077
+ const previousStep = this.currentState.step;
1078
+ this.currentState = this.createStateFromPermissions(nextPermissions);
1079
+ const isFlowReactivating = previousStep === "ready" && this.currentState.step !== "ready";
1080
+ if (isFlowReactivating) {
1081
+ this.hasCompleted = false;
1082
+ }
1083
+ this.emitChange("permission-change");
1084
+ this.tryComplete();
1085
+ }
1086
+ async refreshPermissions(reason) {
1087
+ const permissions = await this.dependencies.queryAllPermissions();
1088
+ this.currentState = this.createStateFromPermissions(permissions);
1089
+ this.emitChange(reason);
1090
+ this.tryComplete();
1091
+ return this.currentState;
1092
+ }
1093
+ createStateFromPermissions(permissions) {
1094
+ const isSecureContext = this.dependencies.getIsSecureContext();
1095
+ if (!isSecureContext) {
1096
+ return this.createInsecureContextState();
1097
+ }
1098
+ const grantedCount = countGrantedPermissions(permissions);
1099
+ if (grantedCount === GRANTED_PERMISSION_COUNT) {
1100
+ return {
1101
+ step: "ready",
1102
+ permissions,
1103
+ denialType: "none",
1104
+ isSecureContext: true,
1105
+ isComplete: true,
1106
+ canRetry: false,
1107
+ shouldProbeUnknown: false
1108
+ };
1109
+ }
1110
+ if (hasHardPermissionDenial(permissions)) {
1111
+ const issueCode = getPermissionDeniedIssueCode(permissions);
1112
+ const recoveryData = this.dependencies.createPermissionRecoveryData(issueCode, this.dependencies.getBrowserInfo(), this.language);
1113
+ return {
1114
+ step: "blocked",
1115
+ permissions,
1116
+ denialType: "hard",
1117
+ issueCode,
1118
+ recoveryData,
1119
+ isSecureContext: true,
1120
+ isComplete: true,
1121
+ canRetry: recoveryData.canRetry,
1122
+ shouldProbeUnknown: false
1123
+ };
1124
+ }
1125
+ const shouldProbeUnknown = hasUnknownPermissionState(permissions);
1126
+ const hasPromptState = hasPromptPermissionState(permissions);
1127
+ let canRetry = false;
1128
+ if (shouldProbeUnknown) {
1129
+ canRetry = true;
1130
+ }
1131
+ if (hasPromptState) {
1132
+ canRetry = true;
1133
+ }
1134
+ return {
1135
+ step: "awaiting-user",
1136
+ permissions,
1137
+ denialType: "soft",
1138
+ isSecureContext: true,
1139
+ isComplete: false,
1140
+ canRetry,
1141
+ shouldProbeUnknown
1142
+ };
1143
+ }
1144
+ transitionToBlockedFromFailedProbe() {
1145
+ const issueCode = "permission-denied-both";
1146
+ const recoveryData = this.dependencies.createPermissionRecoveryData(issueCode, this.dependencies.getBrowserInfo(), this.language);
1147
+ this.currentState = {
1148
+ step: "blocked",
1149
+ permissions: this.currentState.permissions,
1150
+ denialType: "hard",
1151
+ issueCode,
1152
+ recoveryData,
1153
+ isSecureContext: true,
1154
+ isComplete: true,
1155
+ canRetry: recoveryData.canRetry,
1156
+ shouldProbeUnknown: false
1157
+ };
1158
+ this.emitChange("probe-complete");
1159
+ this.tryComplete();
1160
+ return this.currentState;
1161
+ }
1162
+ createInsecureContextState() {
1163
+ const issueCode = "insecure-context";
1164
+ const recoveryData = this.dependencies.createPermissionRecoveryData(issueCode, this.dependencies.getBrowserInfo(), this.language);
1165
+ return {
1166
+ step: "blocked",
1167
+ permissions: this.currentState.permissions,
1168
+ denialType: "hard",
1169
+ issueCode,
1170
+ recoveryData,
1171
+ isSecureContext: false,
1172
+ isComplete: true,
1173
+ canRetry: false,
1174
+ shouldProbeUnknown: false
1175
+ };
1176
+ }
1177
+ updateStateForChecking() {
1178
+ this.currentState = {
1179
+ ...this.currentState,
1180
+ step: "checking"
1181
+ };
1182
+ }
1183
+ emitChange(reason) {
1184
+ if (this.callbacks.onChange) {
1185
+ this.callbacks.onChange(this.currentState, reason);
1186
+ }
1187
+ }
1188
+ tryComplete() {
1189
+ if (this.hasCompleted) {
1190
+ return;
1191
+ }
1192
+ if (!this.currentState.isComplete) {
1193
+ return;
1194
+ }
1195
+ this.hasCompleted = true;
1196
+ if (this.callbacks.onComplete) {
1197
+ this.callbacks.onComplete(this.currentState);
1198
+ }
1199
+ }
1200
+ }
707
1201
  // src/core/utils/video-utils.ts
708
1202
  import { BlobSource, Input, MP4 } from "mediabunny";
709
1203
  async function extractVideoDuration(file) {
@@ -1337,6 +1831,7 @@ function revokeProbeWorkerUrl(workerUrl) {
1337
1831
 
1338
1832
  // src/core/processor/worker/types.ts
1339
1833
  var WORKER_MESSAGE_TYPE_PROBE = "probe";
1834
+ var WORKER_MESSAGE_TYPE_WARMUP = "warmup";
1340
1835
  var WORKER_MESSAGE_TYPE_AUDIO_CHUNK = "audioChunk";
1341
1836
  var WORKER_RESPONSE_TYPE_PROBE_RESULT = "probeResult";
1342
1837
  var WORKER_RESPONSE_TYPE_DEBUG_LOG = "debugLog";
@@ -1531,11 +2026,11 @@ function getIsProbeFeaturesComplete(probeResult, requiresWatermark) {
1531
2026
  }
1532
2027
  return true;
1533
2028
  }
1534
- async function checkRecorderSupport(options = {}) {
2029
+ async function checkRecorderSupport(options = {}, dependencies = {}) {
1535
2030
  const requiresAudio = resolveBooleanOption(options.requiresAudio, true);
1536
2031
  const requiresWatermark = resolveBooleanOption(options.requiresWatermark, false);
1537
2032
  if (!shouldUseSupportCache()) {
1538
- return await buildSupportReport(requiresAudio, requiresWatermark);
2033
+ return await buildSupportReport(requiresAudio, requiresWatermark, dependencies);
1539
2034
  }
1540
2035
  const supportCacheKey = createSupportCacheKey(requiresAudio, requiresWatermark);
1541
2036
  const cachedReport = supportReportCache.get(supportCacheKey);
@@ -1546,7 +2041,7 @@ async function checkRecorderSupport(options = {}) {
1546
2041
  if (inflightReport) {
1547
2042
  return await inflightReport;
1548
2043
  }
1549
- const reportPromise = buildSupportReport(requiresAudio, requiresWatermark).then((report) => {
2044
+ const reportPromise = buildSupportReport(requiresAudio, requiresWatermark, dependencies).then((report) => {
1550
2045
  supportReportCache.set(supportCacheKey, report);
1551
2046
  supportReportPromiseCache.delete(supportCacheKey);
1552
2047
  return report;
@@ -1557,13 +2052,13 @@ async function checkRecorderSupport(options = {}) {
1557
2052
  supportReportPromiseCache.set(supportCacheKey, reportPromise);
1558
2053
  return await reportPromise;
1559
2054
  }
1560
- async function buildSupportReport(requiresAudio, requiresWatermark) {
2055
+ async function buildSupportReport(requiresAudio, requiresWatermark, dependencies) {
1561
2056
  const hasWorker = typeof Worker !== "undefined";
1562
2057
  const audioContextClass = getAudioContextClass();
1563
2058
  const hasAudioContext = audioContextClass !== null;
1564
2059
  const hasAudioWorklet = typeof AudioWorkletNode !== "undefined";
1565
2060
  const hasMainThreadMediaStreamTrackProcessor = typeof MediaStreamTrackProcessor !== "undefined";
1566
- const probeResult = await probeWorkerCapabilities(hasWorker);
2061
+ const probeResult = await probeWorkerCapabilities(hasWorker, dependencies);
1567
2062
  const videoPath = resolveVideoPath({
1568
2063
  probeResult,
1569
2064
  hasMainThreadMediaStreamTrackProcessor
@@ -1608,7 +2103,7 @@ async function buildSupportReport(requiresAudio, requiresWatermark) {
1608
2103
  audioPath
1609
2104
  };
1610
2105
  }
1611
- async function probeWorkerCapabilities(hasWorker) {
2106
+ async function probeWorkerCapabilities(hasWorker, dependencies) {
1612
2107
  if (!hasWorker) {
1613
2108
  return getEmptyProbeResult();
1614
2109
  }
@@ -1618,6 +2113,7 @@ async function probeWorkerCapabilities(hasWorker) {
1618
2113
  }
1619
2114
  return await new Promise((resolve) => {
1620
2115
  let resolved = false;
2116
+ const probeTimeoutMilliseconds = dependencies.probeTimeoutMilliseconds ?? PROBE_TIMEOUT_MILLISECONDS;
1621
2117
  const finalize = (result) => {
1622
2118
  if (resolved) {
1623
2119
  return;
@@ -1628,7 +2124,7 @@ async function probeWorkerCapabilities(hasWorker) {
1628
2124
  };
1629
2125
  const timeoutId = setTimeout(() => {
1630
2126
  finalize(getEmptyProbeResult());
1631
- }, PROBE_TIMEOUT_MILLISECONDS);
2127
+ }, probeTimeoutMilliseconds);
1632
2128
  worker.onmessage = (event) => {
1633
2129
  const payload = event.data;
1634
2130
  if (payload.type !== WORKER_RESPONSE_TYPE_PROBE_RESULT) {
@@ -2136,7 +2632,7 @@ function stopLiveTracks(tracks) {
2136
2632
  }
2137
2633
  }
2138
2634
  }
2139
- function stopStreamTracks(stream) {
2635
+ function stopStreamTracks2(stream) {
2140
2636
  stopLiveTracks(stream.getTracks());
2141
2637
  }
2142
2638
  function stopStreamVideoTracks(stream) {
@@ -2153,7 +2649,7 @@ function areTracksLive(videoTrack, audioTrack) {
2153
2649
  }
2154
2650
  function validateTrack(track, trackType, stream) {
2155
2651
  if (!isTrackLive(track)) {
2156
- stopStreamTracks(stream);
2652
+ stopStreamTracks2(stream);
2157
2653
  let readyState = "undefined";
2158
2654
  if (track) {
2159
2655
  readyState = track.readyState;
@@ -2311,7 +2807,7 @@ class CameraStreamBuilder {
2311
2807
  }
2312
2808
  const managerStream = this.dependencies.streamManager.getStream();
2313
2809
  if (!isRecording && managerStream && managerStream !== this.dependencies.getOriginalCameraStream()) {
2314
- stopStreamTracks(managerStream);
2810
+ stopStreamTracks2(managerStream);
2315
2811
  this.dependencies.streamManager.setMediaStream(null);
2316
2812
  }
2317
2813
  if (isRecording) {
@@ -2639,7 +3135,7 @@ class SourceSwitchManager {
2639
3135
  callbacks: this.callbacks,
2640
3136
  streamManager: this.streamManager,
2641
3137
  combineScreenShareWithOriginalAudio: (screenVideoTrack) => this.combineScreenShareWithOriginalAudio(screenVideoTrack),
2642
- stopStreamTracks: (stream) => stopStreamTracks(stream),
3138
+ stopStreamTracks: (stream) => stopStreamTracks2(stream),
2643
3139
  stopStreamVideoTracks: (stream) => stopStreamVideoTracks(stream),
2644
3140
  getCurrentSourceType: () => this.currentSourceType,
2645
3141
  setCurrentSourceType: (sourceType) => {
@@ -2949,26 +3445,48 @@ var DEFAULT_RECORDING_OPTIONS = Object.freeze({
2949
3445
 
2950
3446
  // src/core/stream/stream-manager.ts
2951
3447
  var TRACK_READY_STATE_LIVE2 = "live";
3448
+ var AUDIO_RETRY_DELAY_MILLISECONDS = 300;
3449
+ var AUDIO_MAX_RETRIES = 2;
2952
3450
  var CAMERA_ERROR_CODE_MAP = {
2953
3451
  NotReadableError: "camera.in-use",
2954
3452
  NotFoundError: "camera.not-found",
2955
3453
  NotAllowedError: "camera.permission-denied",
2956
3454
  OverconstrainedError: "camera.overconstrained"
2957
3455
  };
2958
- function classifyCameraError(error) {
3456
+ var AUDIO_ERROR_CODE_MAP = {
3457
+ NotReadableError: "audio.in-use",
3458
+ NotFoundError: "audio.not-found",
3459
+ NotAllowedError: "audio.permission-denied",
3460
+ OverconstrainedError: "audio.overconstrained"
3461
+ };
3462
+ function getErrorName(error) {
2959
3463
  if (error instanceof DOMException) {
2960
- const mappedCode = CAMERA_ERROR_CODE_MAP[error.name];
3464
+ return error.name;
3465
+ }
3466
+ if (error !== null && typeof error === "object" && "name" in error && typeof error.name === "string") {
3467
+ return error.name;
3468
+ }
3469
+ return null;
3470
+ }
3471
+ function classifyCameraError(error) {
3472
+ const errorName = getErrorName(error);
3473
+ if (errorName !== null) {
3474
+ const mappedCode = CAMERA_ERROR_CODE_MAP[errorName];
2961
3475
  if (mappedCode !== undefined) {
2962
3476
  return mappedCode;
2963
3477
  }
2964
3478
  }
2965
- if (error !== null && typeof error === "object" && "name" in error && typeof error.name === "string") {
2966
- const mappedCode = CAMERA_ERROR_CODE_MAP[error.name];
3479
+ return "camera.unknown";
3480
+ }
3481
+ function classifyAudioError(error) {
3482
+ const errorName = getErrorName(error);
3483
+ if (errorName !== null) {
3484
+ const mappedCode = AUDIO_ERROR_CODE_MAP[errorName];
2967
3485
  if (mappedCode !== undefined) {
2968
3486
  return mappedCode;
2969
3487
  }
2970
3488
  }
2971
- return "camera.unknown";
3489
+ return "audio.unknown";
2972
3490
  }
2973
3491
  function createCameraStreamError(error) {
2974
3492
  const message = extractErrorMessage(error);
@@ -2977,6 +3495,20 @@ function createCameraStreamError(error) {
2977
3495
  cameraError.code = classifyCameraError(error);
2978
3496
  return cameraError;
2979
3497
  }
3498
+ function createAudioStreamError(error) {
3499
+ const message = extractErrorMessage(error);
3500
+ const audioError = new Error(message);
3501
+ audioError.name = "AudioError";
3502
+ audioError.code = classifyAudioError(error);
3503
+ return audioError;
3504
+ }
3505
+ function isRetriableAudioError(error) {
3506
+ const errorName = getErrorName(error);
3507
+ return errorName === "NotReadableError";
3508
+ }
3509
+ function delay(milliseconds) {
3510
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
3511
+ }
2980
3512
 
2981
3513
  class StreamManager {
2982
3514
  mediaStream = null;
@@ -2985,8 +3517,46 @@ class StreamManager {
2985
3517
  streamConfig;
2986
3518
  selectedAudioDeviceId = null;
2987
3519
  selectedVideoDeviceId = null;
2988
- constructor(streamConfig = {}) {
3520
+ audioStatus = "pending";
3521
+ audioAcquisitionPromise = null;
3522
+ pendingAudioError = null;
3523
+ acquisitionGeneration = 0;
3524
+ waitMilliseconds;
3525
+ audioRetryDelayMilliseconds;
3526
+ constructor(streamConfig = {}, dependencies = {}) {
2989
3527
  this.streamConfig = { ...DEFAULT_STREAM_CONFIG, ...streamConfig };
3528
+ this.waitMilliseconds = dependencies.waitMilliseconds ?? delay;
3529
+ this.audioRetryDelayMilliseconds = dependencies.audioRetryDelayMilliseconds ?? AUDIO_RETRY_DELAY_MILLISECONDS;
3530
+ }
3531
+ getAudioStatus() {
3532
+ return this.audioStatus;
3533
+ }
3534
+ isAudioReady() {
3535
+ return this.audioStatus === "acquired";
3536
+ }
3537
+ async waitForAudio() {
3538
+ if (this.audioStatus === "acquired") {
3539
+ return;
3540
+ }
3541
+ if (this.audioStatus === "failed") {
3542
+ throw this.pendingAudioError ?? createAudioStreamError(null);
3543
+ }
3544
+ if (this.audioAcquisitionPromise) {
3545
+ await this.audioAcquisitionPromise;
3546
+ }
3547
+ if (this.getAudioStatus() === "failed") {
3548
+ throw this.pendingAudioError ?? createAudioStreamError(null);
3549
+ }
3550
+ }
3551
+ setAudioStatus(status) {
3552
+ if (this.audioStatus === status) {
3553
+ return;
3554
+ }
3555
+ this.audioStatus = status;
3556
+ this.emit("audiostatuschange", { status });
3557
+ }
3558
+ emitAudioTelemetry(event) {
3559
+ this.emit("audiotelemetry", { event });
2990
3560
  }
2991
3561
  getState() {
2992
3562
  return this.state;
@@ -3120,20 +3690,7 @@ class StreamManager {
3120
3690
  this.setState("starting");
3121
3691
  logger.debug("[StreamManager] State set to 'starting'");
3122
3692
  try {
3123
- logger.debug("[StreamManager] Building constraints", {
3124
- selectedVideoDeviceId: this.selectedVideoDeviceId,
3125
- selectedAudioDeviceId: this.selectedAudioDeviceId
3126
- });
3127
- const constraints = {
3128
- video: this.buildVideoConstraints(this.selectedVideoDeviceId),
3129
- audio: this.buildAudioConstraints(this.selectedAudioDeviceId)
3130
- };
3131
- logger.debug("[StreamManager] Requesting media stream with constraints", {
3132
- hasVideo: !!constraints.video,
3133
- hasAudio: !!constraints.audio
3134
- });
3135
- const mediaDevices = requireMediaDevices();
3136
- this.mediaStream = await mediaDevices.getUserMedia(constraints);
3693
+ this.mediaStream = await this.acquireVideoAndAudioStream();
3137
3694
  logger.info("[StreamManager] Media stream obtained", {
3138
3695
  streamId: this.mediaStream.id,
3139
3696
  videoTracks: this.mediaStream.getVideoTracks().length,
@@ -3145,6 +3702,12 @@ class StreamManager {
3145
3702
  this.emit("streamstart", { stream: this.mediaStream });
3146
3703
  return this.mediaStream;
3147
3704
  } catch (error) {
3705
+ if (error instanceof Error && "code" in error && (error.name === "CameraError" || error.name === "AudioError")) {
3706
+ logger.error("[StreamManager] Failed to start stream", error);
3707
+ this.setState("error");
3708
+ this.emit("error", { error });
3709
+ throw error;
3710
+ }
3148
3711
  const err = createCameraStreamError(error);
3149
3712
  logger.error("[StreamManager] Failed to start stream", err);
3150
3713
  this.setState("error");
@@ -3152,6 +3715,187 @@ class StreamManager {
3152
3715
  throw err;
3153
3716
  }
3154
3717
  }
3718
+ async acquireVideoAndAudioStream() {
3719
+ const mediaDevices = requireMediaDevices();
3720
+ const videoConstraints = this.buildVideoConstraints(this.selectedVideoDeviceId);
3721
+ const audioConstraints = this.buildAudioConstraints(this.selectedAudioDeviceId);
3722
+ this.setAudioStatus("pending");
3723
+ this.pendingAudioError = null;
3724
+ logger.debug("[StreamManager] Attempting combined getUserMedia", {
3725
+ selectedVideoDeviceId: this.selectedVideoDeviceId,
3726
+ selectedAudioDeviceId: this.selectedAudioDeviceId
3727
+ });
3728
+ try {
3729
+ const stream = await mediaDevices.getUserMedia({
3730
+ video: videoConstraints,
3731
+ audio: audioConstraints
3732
+ });
3733
+ const hasVideo = stream.getVideoTracks().length > 0;
3734
+ const hasAudio = stream.getAudioTracks().length > 0;
3735
+ if (hasVideo && hasAudio) {
3736
+ logger.debug("[StreamManager] Combined getUserMedia succeeded with video + audio");
3737
+ this.setAudioStatus("acquired");
3738
+ return stream;
3739
+ }
3740
+ logger.warn("[StreamManager] Combined getUserMedia returned incomplete stream", {
3741
+ videoTracks: stream.getVideoTracks().length,
3742
+ audioTracks: stream.getAudioTracks().length
3743
+ });
3744
+ this.stopStreamTracks(stream);
3745
+ } catch (combinedError) {
3746
+ const combinedErrorName = getErrorName(combinedError);
3747
+ const combinedErrorMessage = extractErrorMessage(combinedError);
3748
+ logger.warn("[StreamManager] Combined getUserMedia failed, falling back to separate acquisition", {
3749
+ error: combinedErrorMessage,
3750
+ errorName: combinedErrorName
3751
+ });
3752
+ this.emitAudioTelemetry({
3753
+ name: "audio.acquisition.fallback",
3754
+ properties: {
3755
+ reason: "combined_getUserMedia_failed",
3756
+ originalError: combinedErrorMessage,
3757
+ originalErrorName: combinedErrorName,
3758
+ selectedAudioDeviceId: this.selectedAudioDeviceId,
3759
+ selectedVideoDeviceId: this.selectedVideoDeviceId
3760
+ }
3761
+ });
3762
+ }
3763
+ let videoStream;
3764
+ try {
3765
+ videoStream = await mediaDevices.getUserMedia({
3766
+ video: videoConstraints,
3767
+ audio: false
3768
+ });
3769
+ logger.debug("[StreamManager] Video-only stream acquired for preview");
3770
+ } catch (videoError) {
3771
+ this.setAudioStatus("failed");
3772
+ throw createCameraStreamError(videoError);
3773
+ }
3774
+ this.mediaStream = videoStream;
3775
+ this.acquisitionGeneration++;
3776
+ const generation = this.acquisitionGeneration;
3777
+ this.audioAcquisitionPromise = this.acquireAudioInBackground(mediaDevices, audioConstraints, generation);
3778
+ return videoStream;
3779
+ }
3780
+ async acquireAudioInBackground(mediaDevices, audioConstraints, generation) {
3781
+ try {
3782
+ const audioTrack = await this.acquireAudioTrackWithRetry(mediaDevices, audioConstraints);
3783
+ if (!this.mediaStream || this.acquisitionGeneration !== generation) {
3784
+ logger.debug("[StreamManager] Audio acquired but stream was replaced/stopped, discarding track", { generation, currentGeneration: this.acquisitionGeneration });
3785
+ audioTrack.stop();
3786
+ return;
3787
+ }
3788
+ this.mediaStream.addTrack(audioTrack);
3789
+ logger.info("[StreamManager] Audio track added to stream", {
3790
+ audioTrackId: audioTrack.id,
3791
+ audioTrackLabel: audioTrack.label,
3792
+ totalAudioTracks: this.mediaStream.getAudioTracks().length
3793
+ });
3794
+ this.setAudioStatus("acquired");
3795
+ } catch (error) {
3796
+ if (this.acquisitionGeneration !== generation) {
3797
+ return;
3798
+ }
3799
+ const audioError = error instanceof Error && "code" in error ? error : createAudioStreamError(error);
3800
+ this.pendingAudioError = audioError;
3801
+ this.setAudioStatus("failed");
3802
+ logger.error("[StreamManager] Background audio acquisition failed", audioError);
3803
+ this.emitAudioTelemetry({
3804
+ name: "audio.acquisition.failed",
3805
+ properties: {
3806
+ errorCode: audioError.code,
3807
+ errorName: audioError.name,
3808
+ maxRetries: AUDIO_MAX_RETRIES,
3809
+ retryDelayMs: this.audioRetryDelayMilliseconds,
3810
+ selectedAudioDeviceId: this.selectedAudioDeviceId
3811
+ },
3812
+ error: audioError
3813
+ });
3814
+ this.emit("error", { error: audioError });
3815
+ } finally {
3816
+ this.audioAcquisitionPromise = null;
3817
+ }
3818
+ }
3819
+ resolveAudioConstraintsForAttempt(audioConstraints, attempt) {
3820
+ if (attempt !== AUDIO_MAX_RETRIES) {
3821
+ return audioConstraints;
3822
+ }
3823
+ if (typeof audioConstraints !== "object") {
3824
+ return audioConstraints;
3825
+ }
3826
+ const { deviceId: _deviceId, ...relaxed } = audioConstraints;
3827
+ const hasOtherConstraints = Object.keys(relaxed).length > 0;
3828
+ const result = hasOtherConstraints ? relaxed : true;
3829
+ logger.debug("[StreamManager] Audio retry with relaxed constraints (no deviceId)", { attempt });
3830
+ return result;
3831
+ }
3832
+ async acquireAudioTrackWithRetry(mediaDevices, audioConstraints) {
3833
+ let lastError = null;
3834
+ for (let attempt = 0;attempt <= AUDIO_MAX_RETRIES; attempt++) {
3835
+ try {
3836
+ const constraintsForAttempt = this.resolveAudioConstraintsForAttempt(audioConstraints, attempt);
3837
+ const audioStream = await mediaDevices.getUserMedia({
3838
+ video: false,
3839
+ audio: constraintsForAttempt
3840
+ });
3841
+ const audioTrack = audioStream.getAudioTracks()[0];
3842
+ if (!audioTrack) {
3843
+ this.stopStreamTracks(audioStream);
3844
+ throw new Error("getUserMedia returned no audio tracks");
3845
+ }
3846
+ for (const track of audioStream.getVideoTracks()) {
3847
+ track.stop();
3848
+ }
3849
+ logger.debug("[StreamManager] Audio track acquired", {
3850
+ attempt,
3851
+ trackId: audioTrack.id,
3852
+ trackLabel: audioTrack.label
3853
+ });
3854
+ if (attempt > 0) {
3855
+ this.emitAudioTelemetry({
3856
+ name: "audio.acquisition.recovered",
3857
+ properties: {
3858
+ successAttempt: attempt,
3859
+ totalAttempts: attempt + 1,
3860
+ audioTrackLabel: audioTrack.label,
3861
+ usedRelaxedConstraints: attempt === AUDIO_MAX_RETRIES
3862
+ }
3863
+ });
3864
+ }
3865
+ return audioTrack;
3866
+ } catch (error) {
3867
+ lastError = error;
3868
+ const errorMessage = extractErrorMessage(error);
3869
+ const errorName = getErrorName(error);
3870
+ const retriable = isRetriableAudioError(error);
3871
+ logger.warn("[StreamManager] Audio acquisition failed", {
3872
+ attempt,
3873
+ maxRetries: AUDIO_MAX_RETRIES,
3874
+ error: errorMessage,
3875
+ errorName,
3876
+ isRetriable: retriable
3877
+ });
3878
+ this.emitAudioTelemetry({
3879
+ name: "audio.acquisition.retry",
3880
+ properties: {
3881
+ attempt,
3882
+ maxRetries: AUDIO_MAX_RETRIES,
3883
+ errorMessage,
3884
+ errorName: errorName ?? "unknown",
3885
+ isRetriable: retriable,
3886
+ usedRelaxedConstraints: attempt === AUDIO_MAX_RETRIES,
3887
+ willRetry: attempt < AUDIO_MAX_RETRIES && retriable
3888
+ }
3889
+ });
3890
+ const canRetry = attempt < AUDIO_MAX_RETRIES && retriable;
3891
+ if (!canRetry) {
3892
+ break;
3893
+ }
3894
+ await this.waitMilliseconds(this.audioRetryDelayMilliseconds);
3895
+ }
3896
+ }
3897
+ throw createAudioStreamError(lastError);
3898
+ }
3155
3899
  stopStream() {
3156
3900
  if (this.mediaStream) {
3157
3901
  for (const track of this.mediaStream.getTracks()) {
@@ -3159,6 +3903,10 @@ class StreamManager {
3159
3903
  }
3160
3904
  this.mediaStream = null;
3161
3905
  }
3906
+ this.audioStatus = "pending";
3907
+ this.pendingAudioError = null;
3908
+ this.audioAcquisitionPromise = null;
3909
+ this.acquisitionGeneration++;
3162
3910
  if (this.state !== "idle") {
3163
3911
  this.setState("idle");
3164
3912
  this.emit("streamstop", undefined);
@@ -3511,9 +4259,13 @@ class StreamRecordingState {
3511
4259
  return Promise.resolve();
3512
4260
  }
3513
4261
  const hasAudioTracks = mediaStream.getAudioTracks().length > 0;
4262
+ if (!hasAudioTracks) {
4263
+ logger.error("[StreamRecordingState] Cannot start recording without audio tracks");
4264
+ throw new Error("Cannot start recording: no audio track available. Please check your microphone.");
4265
+ }
3514
4266
  const requiresWatermark = config.watermark !== undefined;
3515
4267
  const supportReport = await this.dependencies.checkRecorderSupport({
3516
- requiresAudio: hasAudioTracks,
4268
+ requiresAudio: true,
3517
4269
  requiresWatermark
3518
4270
  });
3519
4271
  if (!supportReport.isSupported) {
@@ -3901,6 +4653,15 @@ class CameraStreamManager {
3901
4653
  getCurrentVideoSource() {
3902
4654
  return this.recordingState.getCurrentVideoSource();
3903
4655
  }
4656
+ getAudioStatus() {
4657
+ return this.streamManager.getAudioStatus();
4658
+ }
4659
+ isAudioReady() {
4660
+ return this.streamManager.isAudioReady();
4661
+ }
4662
+ async waitForAudio() {
4663
+ return await this.streamManager.waitForAudio();
4664
+ }
3904
4665
  destroy() {
3905
4666
  this.recordingState.destroy();
3906
4667
  this.streamManager.destroy();
@@ -3909,7 +4670,7 @@ class CameraStreamManager {
3909
4670
  // package.json
3910
4671
  var package_default = {
3911
4672
  name: "@vidtreo/recorder",
3912
- version: "1.3.4",
4673
+ version: "1.4.1",
3913
4674
  type: "module",
3914
4675
  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.",
3915
4676
  main: "./dist/index.js",
@@ -3936,6 +4697,7 @@ var package_default = {
3936
4697
  test: "bun test --concurrent",
3937
4698
  "test:watch": "bun test --watch --concurrent",
3938
4699
  "test:coverage": "bun test --coverage --concurrent",
4700
+ "test:bench:recording-start": 'RUN_RECORDING_BENCHMARKS=1 bun test --concurrent "tests/core/recording/recording-start.micro-benchmark.test.ts" "tests/core/recording/recording-start.realistic-benchmark.test.ts"',
3939
4701
  "test:isolation": "bun test --bail",
3940
4702
  "test:random": "bun test --bail --rerun-each 2"
3941
4703
  },
@@ -3991,7 +4753,6 @@ var BATCH_FLUSH_INTERVAL_MS = 1000;
3991
4753
  var THROTTLE_WINDOW_MS = 5000;
3992
4754
  var MAX_RETRY_ATTEMPTS = 3;
3993
4755
  var MAX_PENDING_EVENTS = 100;
3994
- var ONE_TIME_EVENT_CACHE = new Map;
3995
4756
  function resolveInstallationId(dependencies) {
3996
4757
  const storageProvider = dependencies.storageProvider;
3997
4758
  const stored = storageProvider?.getItem(TELEMETRY_STORAGE_KEY);
@@ -4044,7 +4805,11 @@ var TELEMETRY_EVENT_CATEGORY_MAP = {
4044
4805
  "source.switch.requested": "interaction",
4045
4806
  "source.switch.succeeded": "interaction",
4046
4807
  "source.switch.failed": "error",
4047
- "stream.error": "error"
4808
+ "stream.error": "error",
4809
+ "audio.acquisition.fallback": "lifecycle",
4810
+ "audio.acquisition.retry": "lifecycle",
4811
+ "audio.acquisition.recovered": "lifecycle",
4812
+ "audio.acquisition.failed": "error"
4048
4813
  };
4049
4814
 
4050
4815
  class TelemetryClient {
@@ -4055,6 +4820,7 @@ class TelemetryClient {
4055
4820
  flushTimeoutId = null;
4056
4821
  throttledEventTimestamps = new Map;
4057
4822
  retryCountMap = new Map;
4823
+ oneTimeEventCache = new Map;
4058
4824
  constructor(config, dependencies) {
4059
4825
  this.config = config;
4060
4826
  this.dependencies = dependencies;
@@ -4157,7 +4923,7 @@ class TelemetryClient {
4157
4923
  shouldSkipEvent(name, timestamp) {
4158
4924
  if (this.isOneTimeEvent(name)) {
4159
4925
  const cacheKey = this.getOneTimeCacheKey(name);
4160
- const wasSent = ONE_TIME_EVENT_CACHE.get(cacheKey);
4926
+ const wasSent = this.oneTimeEventCache.get(cacheKey);
4161
4927
  if (wasSent) {
4162
4928
  logger.debug("Telemetry event skipped (dedupe)", {
4163
4929
  event: name
@@ -4180,7 +4946,7 @@ class TelemetryClient {
4180
4946
  markEventTracking(name, timestamp) {
4181
4947
  if (this.isOneTimeEvent(name)) {
4182
4948
  const cacheKey = this.getOneTimeCacheKey(name);
4183
- ONE_TIME_EVENT_CACHE.set(cacheKey, true);
4949
+ this.oneTimeEventCache.set(cacheKey, true);
4184
4950
  }
4185
4951
  if (this.isThrottledEvent(name)) {
4186
4952
  this.throttledEventTimestamps = this.updateNumberMap(this.throttledEventTimestamps, name, timestamp);
@@ -4521,16 +5287,16 @@ class UploadQueueManager {
4521
5287
  const retryableUploads = failedUploads.filter((upload) => upload.retryCount < MAX_RETRIES2);
4522
5288
  if (retryableUploads.length > 0) {
4523
5289
  const upload = this.getOldestFailedUpload(retryableUploads);
4524
- const delay = this.calculateRetryDelay(upload.retryCount);
5290
+ const delay2 = this.calculateRetryDelay(upload.retryCount);
4525
5291
  const timeSinceLastAttempt = Date.now() - upload.updatedAt;
4526
- if (timeSinceLastAttempt >= delay) {
5292
+ if (timeSinceLastAttempt >= delay2) {
4527
5293
  await this.storageService.updateUploadStatus(upload.id, {
4528
5294
  status: "pending",
4529
5295
  retryCount: upload.retryCount
4530
5296
  });
4531
5297
  await this.processUpload(upload);
4532
5298
  } else {
4533
- const remainingDelay = delay - timeSinceLastAttempt;
5299
+ const remainingDelay = delay2 - timeSinceLastAttempt;
4534
5300
  this.scheduleRetry(remainingDelay);
4535
5301
  }
4536
5302
  }
@@ -4602,16 +5368,16 @@ class UploadQueueManager {
4602
5368
  if (retryCount >= MAX_RETRIES2) {
4603
5369
  this.callbacks.onUploadError?.(upload.id, new Error(`Upload failed after ${MAX_RETRIES2} attempts: ${errorMessage}`));
4604
5370
  } else {
4605
- const delay = this.calculateRetryDelay(retryCount);
4606
- this.scheduleRetry(delay);
5371
+ const delay2 = this.calculateRetryDelay(retryCount);
5372
+ this.scheduleRetry(delay2);
4607
5373
  }
4608
5374
  }
4609
5375
  }
4610
5376
  calculateRetryDelay(retryCount) {
4611
- const delay = INITIAL_RETRY_DELAY * RETRY_MULTIPLIER ** (retryCount - 1);
4612
- return Math.min(delay, MAX_RETRY_DELAY);
5377
+ const delay2 = INITIAL_RETRY_DELAY * RETRY_MULTIPLIER ** (retryCount - 1);
5378
+ return Math.min(delay2, MAX_RETRY_DELAY);
4613
5379
  }
4614
- scheduleRetry(delay) {
5380
+ scheduleRetry(delay2) {
4615
5381
  this.clearTimer(this.retryTimeoutId, clearTimeout);
4616
5382
  this.retryTimeoutId = window.setTimeout(() => {
4617
5383
  this.retryTimeoutId = null;
@@ -4619,7 +5385,7 @@ class UploadQueueManager {
4619
5385
  const errorMessage = extractErrorMessage(error);
4620
5386
  this.callbacks.onUploadError?.("scheduled-retry", new Error(errorMessage));
4621
5387
  });
4622
- }, delay);
5388
+ }, delay2);
4623
5389
  }
4624
5390
  clearTimer(timerId, clearFn) {
4625
5391
  if (timerId !== null) {
@@ -13993,20 +14759,31 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
13993
14759
  if (!compositionPlan.needsComposition) {
13994
14760
  return { frameToProcess: parameters.videoFrame, imageBitmap: null };
13995
14761
  }
13996
- const dimensions = this.getValidFrameDimensions(parameters.videoFrame, compositionPlan.rotationDegrees);
13997
- if (!dimensions) {
14762
+ const outputDimensions = this.getOutputDimensions(parameters.videoFrame, compositionPlan.rotationDegrees, parameters.config);
14763
+ if (!outputDimensions) {
13998
14764
  return { frameToProcess: parameters.videoFrame, imageBitmap: null };
13999
14765
  }
14000
- const width = dimensions.width;
14001
- const height = dimensions.height;
14766
+ const width = outputDimensions.width;
14767
+ const height = outputDimensions.height;
14768
+ let fit = null;
14769
+ const sourceDimensions = this.getFrameDimensions(parameters.videoFrame, compositionPlan.rotationDegrees);
14770
+ const dimensionsMismatch = sourceDimensions.width !== width || sourceDimensions.height !== height;
14771
+ if (compositionPlan.needsResizing || dimensionsMismatch) {
14772
+ fit = this.calculateContainFit(sourceDimensions.width, sourceDimensions.height, width, height);
14773
+ }
14002
14774
  const context = this.ensureCompositionCanvas(width, height);
14003
14775
  context.clearRect(0, 0, width, height);
14776
+ if (fit && (fit.drawX > 0 || fit.drawY > 0)) {
14777
+ context.fillStyle = "#000000";
14778
+ context.fillRect(0, 0, width, height);
14779
+ }
14004
14780
  this.drawVideoFrame({
14005
14781
  context,
14006
14782
  videoFrame: parameters.videoFrame,
14007
14783
  rotationDegrees: compositionPlan.rotationDegrees,
14008
14784
  width,
14009
- height
14785
+ height,
14786
+ fit
14010
14787
  });
14011
14788
  this.applyOverlayIfNeeded(context, width, compositionPlan.shouldApplyOverlay, parameters.overlayConfig);
14012
14789
  this.applyWatermarkIfNeeded({
@@ -14025,6 +14802,7 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
14025
14802
  if (parameters.config.watermark && this.watermarkCanvas) {
14026
14803
  needsWatermark = true;
14027
14804
  }
14805
+ const needsResizing = this.detectResizingNeed(parameters.videoFrame, rotationDegrees, parameters.config);
14028
14806
  let needsComposition = false;
14029
14807
  if (parameters.shouldApplyOverlay) {
14030
14808
  needsComposition = true;
@@ -14035,30 +14813,17 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
14035
14813
  if (shouldRotateFrame) {
14036
14814
  needsComposition = true;
14037
14815
  }
14816
+ if (needsResizing) {
14817
+ needsComposition = true;
14818
+ }
14038
14819
  return {
14039
14820
  rotationDegrees,
14040
14821
  shouldApplyOverlay: parameters.shouldApplyOverlay,
14041
14822
  needsWatermark,
14823
+ needsResizing,
14042
14824
  needsComposition
14043
14825
  };
14044
14826
  }
14045
- getValidFrameDimensions(videoFrame, rotationDegrees) {
14046
- const dimensions = this.getFrameDimensions(videoFrame, rotationDegrees);
14047
- const width = dimensions.width;
14048
- const height = dimensions.height;
14049
- let hasInvalidDimensions = false;
14050
- if (width <= 0) {
14051
- hasInvalidDimensions = true;
14052
- }
14053
- if (height <= 0) {
14054
- hasInvalidDimensions = true;
14055
- }
14056
- if (hasInvalidDimensions) {
14057
- this.logger.warn(\`\${RECORDER_WORKER_LOG_PREFIX} Invalid video frame dimensions, skipping composition\`, { width, height });
14058
- return null;
14059
- }
14060
- return { width, height };
14061
- }
14062
14827
  applyOverlayIfNeeded(context, videoWidth, shouldApplyOverlay, overlayConfig) {
14063
14828
  if (!(shouldApplyOverlay && overlayConfig)) {
14064
14829
  return;
@@ -14242,25 +15007,60 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
14242
15007
  return { width, height };
14243
15008
  }
14244
15009
  drawVideoFrame(parameters) {
14245
- const { context, videoFrame, rotationDegrees, width, height } = parameters;
15010
+ const { context, videoFrame, rotationDegrees, width, height, fit } = parameters;
14246
15011
  const sourceWidth = videoFrame.displayWidth;
14247
15012
  const sourceHeight = videoFrame.displayHeight;
15013
+ if (sourceWidth <= 0 || sourceHeight <= 0) {
15014
+ return;
15015
+ }
14248
15016
  context.setTransform(1, 0, 0, 1, 0, 0);
14249
- if (rotationDegrees === ROTATION_DEGREES_90) {
14250
- context.translate(width, 0);
14251
- context.rotate(ROTATION_RADIANS_90);
15017
+ if (!fit) {
15018
+ if (rotationDegrees === ROTATION_DEGREES_90) {
15019
+ context.translate(width, 0);
15020
+ context.rotate(ROTATION_RADIANS_90);
15021
+ context.drawImage(videoFrame, 0, 0, sourceWidth, sourceHeight);
15022
+ context.setTransform(1, 0, 0, 1, 0, 0);
15023
+ return;
15024
+ }
15025
+ if (rotationDegrees === ROTATION_DEGREES_270) {
15026
+ context.translate(0, height);
15027
+ context.rotate(ROTATION_RADIANS_270);
15028
+ context.drawImage(videoFrame, 0, 0, sourceWidth, sourceHeight);
15029
+ context.setTransform(1, 0, 0, 1, 0, 0);
15030
+ return;
15031
+ }
14252
15032
  context.drawImage(videoFrame, 0, 0, sourceWidth, sourceHeight);
14253
- context.setTransform(1, 0, 0, 1, 0, 0);
14254
15033
  return;
14255
15034
  }
14256
- if (rotationDegrees === ROTATION_DEGREES_270) {
14257
- context.translate(0, height);
14258
- context.rotate(ROTATION_RADIANS_270);
14259
- context.drawImage(videoFrame, 0, 0, sourceWidth, sourceHeight);
14260
- context.setTransform(1, 0, 0, 1, 0, 0);
15035
+ if (rotationDegrees === ROTATION_DEGREES_0) {
15036
+ context.drawImage(videoFrame, fit.drawX, fit.drawY, fit.drawWidth, fit.drawHeight);
14261
15037
  return;
14262
15038
  }
14263
- context.drawImage(videoFrame, 0, 0, sourceWidth, sourceHeight);
15039
+ const centerX = fit.drawX + fit.drawWidth / DOUBLE_VALUE;
15040
+ const centerY = fit.drawY + fit.drawHeight / DOUBLE_VALUE;
15041
+ context.translate(centerX, centerY);
15042
+ if (rotationDegrees === ROTATION_DEGREES_90) {
15043
+ context.rotate(ROTATION_RADIANS_90);
15044
+ } else if (rotationDegrees === ROTATION_DEGREES_270) {
15045
+ context.rotate(ROTATION_RADIANS_270);
15046
+ } else {
15047
+ context.rotate(Math.PI * rotationDegrees / ROTATION_DEGREES_180);
15048
+ }
15049
+ const isSwappedRotation = rotationDegrees === ROTATION_DEGREES_90 || rotationDegrees === ROTATION_DEGREES_270;
15050
+ const scaleX = isSwappedRotation ? fit.drawWidth / sourceHeight : fit.drawWidth / sourceWidth;
15051
+ const scaleY = isSwappedRotation ? fit.drawHeight / sourceWidth : fit.drawHeight / sourceHeight;
15052
+ context.scale(scaleX, scaleY);
15053
+ context.drawImage(videoFrame, -sourceWidth / DOUBLE_VALUE, -sourceHeight / DOUBLE_VALUE, sourceWidth, sourceHeight);
15054
+ context.setTransform(1, 0, 0, 1, 0, 0);
15055
+ }
15056
+ detectResizingNeed(videoFrame, rotationDegrees, config) {
15057
+ return detectResizingNeed(videoFrame, rotationDegrees, config);
15058
+ }
15059
+ getOutputDimensions(videoFrame, rotationDegrees, config) {
15060
+ return getOutputDimensions(videoFrame, rotationDegrees, config);
15061
+ }
15062
+ calculateContainFit(sourceWidth, sourceHeight, targetWidth, targetHeight) {
15063
+ return calculateContainFit(sourceWidth, sourceHeight, targetWidth, targetHeight);
14264
15064
  }
14265
15065
  logWatermarkError(url2, error) {
14266
15066
  const errorMessage = extractErrorMessage(error);
@@ -14270,6 +15070,78 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
14270
15070
  });
14271
15071
  }
14272
15072
  }
15073
+ function calculateContainFit(sourceWidth, sourceHeight, targetWidth, targetHeight) {
15074
+ if (sourceWidth <= 0 || sourceHeight <= 0 || targetWidth <= 0 || targetHeight <= 0) {
15075
+ return {
15076
+ drawX: 0,
15077
+ drawY: 0,
15078
+ drawWidth: targetWidth,
15079
+ drawHeight: targetHeight
15080
+ };
15081
+ }
15082
+ const sourceAspect = sourceWidth / sourceHeight;
15083
+ const targetAspect = targetWidth / targetHeight;
15084
+ let drawWidth;
15085
+ let drawHeight;
15086
+ if (sourceAspect > targetAspect) {
15087
+ drawWidth = targetWidth;
15088
+ drawHeight = targetWidth / sourceAspect;
15089
+ } else {
15090
+ drawHeight = targetHeight;
15091
+ drawWidth = targetHeight * sourceAspect;
15092
+ }
15093
+ const roundedDrawWidth = Math.round(drawWidth);
15094
+ const roundedDrawHeight = Math.round(drawHeight);
15095
+ const drawX = (targetWidth - roundedDrawWidth) / 2;
15096
+ const drawY = (targetHeight - roundedDrawHeight) / 2;
15097
+ return {
15098
+ drawX: Math.round(drawX),
15099
+ drawY: Math.round(drawY),
15100
+ drawWidth: roundedDrawWidth,
15101
+ drawHeight: roundedDrawHeight
15102
+ };
15103
+ }
15104
+ function detectResizingNeed(videoFrame, rotationDegrees, config) {
15105
+ if (typeof config.width !== "number" || typeof config.height !== "number") {
15106
+ return false;
15107
+ }
15108
+ if (config.width <= 0 || config.height <= 0) {
15109
+ return false;
15110
+ }
15111
+ let frameWidth = videoFrame.displayWidth;
15112
+ let frameHeight = videoFrame.displayHeight;
15113
+ if (rotationDegrees === 90 || rotationDegrees === 270) {
15114
+ frameWidth = videoFrame.displayHeight;
15115
+ frameHeight = videoFrame.displayWidth;
15116
+ }
15117
+ if (frameWidth === config.width && frameHeight === config.height) {
15118
+ return false;
15119
+ }
15120
+ const sourceAspect = frameWidth / frameHeight;
15121
+ const targetAspect = config.width / config.height;
15122
+ const aspectRatioTolerance = 0.02;
15123
+ if (Math.abs(sourceAspect - targetAspect) > aspectRatioTolerance) {
15124
+ return true;
15125
+ }
15126
+ const sourcePixels = frameWidth * frameHeight;
15127
+ const targetPixels = config.width * config.height;
15128
+ return sourcePixels > targetPixels;
15129
+ }
15130
+ function getOutputDimensions(videoFrame, rotationDegrees, config) {
15131
+ if (typeof config.width === "number" && config.width > 0 && typeof config.height === "number" && config.height > 0) {
15132
+ return { width: config.width, height: config.height };
15133
+ }
15134
+ let width = videoFrame.displayWidth;
15135
+ let height = videoFrame.displayHeight;
15136
+ if (rotationDegrees === 90 || rotationDegrees === 270) {
15137
+ width = videoFrame.displayHeight;
15138
+ height = videoFrame.displayWidth;
15139
+ }
15140
+ if (width <= 0 || height <= 0) {
15141
+ return null;
15142
+ }
15143
+ return { width, height };
15144
+ }
14273
15145
 
14274
15146
  // src/core/processor/worker/recording-integrity.ts
14275
15147
  var EXCESSIVE_FRAME_ERROR_RATIO_THRESHOLD = 0.5;
@@ -14348,7 +15220,6 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
14348
15220
 
14349
15221
  // src/core/processor/worker/timestamp-manager.ts
14350
15222
  var DEFAULT_FRAME_RATE = 30;
14351
- var DEFAULT_KEY_FRAME_INTERVAL_SECONDS = 5;
14352
15223
  var MILLISECONDS_PER_SECOND2 = 1000;
14353
15224
  var MICROSECONDS_PER_SECOND = 1e6;
14354
15225
  var MAX_LEAD_SECONDS = 0.05;
@@ -14363,7 +15234,6 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
14363
15234
  lastVideoTimestamp = 0;
14364
15235
  baseVideoTimestamp = null;
14365
15236
  frameCount = 0;
14366
- lastKeyFrameTimestamp = 0;
14367
15237
  forceNextKeyFrame = false;
14368
15238
  driftOffset = 0;
14369
15239
  logger;
@@ -14381,7 +15251,6 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
14381
15251
  this.lastVideoTimestamp = 0;
14382
15252
  this.baseVideoTimestamp = null;
14383
15253
  this.frameCount = 0;
14384
- this.lastKeyFrameTimestamp = 0;
14385
15254
  this.forceNextKeyFrame = false;
14386
15255
  this.driftOffset = 0;
14387
15256
  }
@@ -14470,36 +15339,25 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
14470
15339
  prepareFrameTiming(parameters) {
14471
15340
  const frameDuration = 1 / this.frameRate;
14472
15341
  let adjustedTimestamp = parameters.frameTimestamp + this.driftOffset;
14473
- if (adjustedTimestamp - parameters.lastAudioTimestamp > MAX_LEAD_SECONDS) {
14474
- adjustedTimestamp = parameters.lastAudioTimestamp + MAX_LEAD_SECONDS;
14475
- }
14476
- if (parameters.lastAudioTimestamp - adjustedTimestamp > MAX_LAG_SECONDS) {
14477
- adjustedTimestamp = parameters.lastAudioTimestamp - MAX_LAG_SECONDS;
14478
- }
14479
15342
  const monotonicTimestamp = this.lastVideoTimestamp + frameDuration;
15343
+ if (adjustedTimestamp < monotonicTimestamp) {
15344
+ adjustedTimestamp = monotonicTimestamp;
15345
+ }
14480
15346
  let finalTimestamp = adjustedTimestamp;
14481
- if (finalTimestamp < monotonicTimestamp) {
14482
- finalTimestamp = monotonicTimestamp;
15347
+ if (finalTimestamp - parameters.lastAudioTimestamp > MAX_LEAD_SECONDS) {
15348
+ finalTimestamp = parameters.lastAudioTimestamp + MAX_LEAD_SECONDS;
14483
15349
  }
14484
- let keyFrameIntervalSeconds = parameters.keyFrameIntervalSeconds;
14485
- if (!(keyFrameIntervalSeconds > 0)) {
14486
- keyFrameIntervalSeconds = DEFAULT_KEY_FRAME_INTERVAL_SECONDS;
15350
+ if (parameters.lastAudioTimestamp - finalTimestamp > MAX_LAG_SECONDS) {
15351
+ finalTimestamp = parameters.lastAudioTimestamp - MAX_LAG_SECONDS;
14487
15352
  }
14488
- let keyFrameIntervalFrames = Math.round(keyFrameIntervalSeconds * this.frameRate);
14489
- if (keyFrameIntervalFrames < 1) {
14490
- keyFrameIntervalFrames = 1;
15353
+ const minimumTimestamp = this.lastVideoTimestamp + frameDuration;
15354
+ if (finalTimestamp < minimumTimestamp) {
15355
+ finalTimestamp = minimumTimestamp;
14491
15356
  }
14492
- const timeSinceLastKeyFrame = finalTimestamp - this.lastKeyFrameTimestamp;
14493
15357
  let isKeyFrame = false;
14494
15358
  if (this.forceNextKeyFrame) {
14495
15359
  isKeyFrame = true;
14496
15360
  }
14497
- if (timeSinceLastKeyFrame >= keyFrameIntervalSeconds) {
14498
- isKeyFrame = true;
14499
- }
14500
- if (this.frameCount % keyFrameIntervalFrames === 0) {
14501
- isKeyFrame = true;
14502
- }
14503
15361
  this.driftOffset *= DRIFT_OFFSET_DECAY_FACTOR;
14504
15362
  return {
14505
15363
  finalTimestamp,
@@ -14511,7 +15369,6 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
14511
15369
  this.frameCount += 1;
14512
15370
  this.lastVideoTimestamp = parameters.finalTimestamp;
14513
15371
  if (parameters.isKeyFrame) {
14514
- this.lastKeyFrameTimestamp = parameters.finalTimestamp;
14515
15372
  this.forceNextKeyFrame = false;
14516
15373
  }
14517
15374
  let shouldLogDrift = false;
@@ -14554,6 +15411,7 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
14554
15411
 
14555
15412
  // src/core/processor/worker/types.ts
14556
15413
  var WORKER_MESSAGE_TYPE_PROBE = "probe";
15414
+ var WORKER_MESSAGE_TYPE_WARMUP = "warmup";
14557
15415
  var WORKER_MESSAGE_TYPE_AUDIO_CHUNK = "audioChunk";
14558
15416
  var WORKER_RESPONSE_TYPE_PROBE_RESULT = "probeResult";
14559
15417
  var WORKER_AUDIO_SAMPLE_FORMAT_F32_PLANAR = "f32-planar";
@@ -14738,6 +15596,7 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
14738
15596
  expectedAudioSampleRate = null;
14739
15597
  pendingWriteCount = 0;
14740
15598
  resolvedHardwareAcceleration = VIDEO_HARDWARE_ACCELERATION_PREFERENCE;
15599
+ hwAccelCacheKey = null;
14741
15600
  consecutiveFrameErrors = 0;
14742
15601
  videoProcessingRunId = 0;
14743
15602
  totalFrameErrors = 0;
@@ -14820,6 +15679,13 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
14820
15679
  case WORKER_MESSAGE_TYPE_PROBE:
14821
15680
  this.handleProbe();
14822
15681
  return;
15682
+ case WORKER_MESSAGE_TYPE_WARMUP:
15683
+ this.resolveHardwareAcceleration(message.config).then((result) => {
15684
+ this.resolvedHardwareAcceleration = result;
15685
+ }).catch((error) => {
15686
+ logger.warn("[RecorderWorker] Warmup hardware acceleration probe failed", { error: extractErrorMessage(error) });
15687
+ });
15688
+ return;
14823
15689
  case "start":
14824
15690
  this.handleStartMessage(message);
14825
15691
  return;
@@ -14979,7 +15845,6 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
14979
15845
  }
14980
15846
  createVideoSource(config, hardwareAcceleration) {
14981
15847
  const fps = this.timestampManager.getFrameRate();
14982
- const keyFrameIntervalSeconds = config.keyFrameInterval;
14983
15848
  const videoSourceOptions = {
14984
15849
  codec: config.codec,
14985
15850
  width: config.width,
@@ -14987,10 +15852,10 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
14987
15852
  sizeChangeBehavior: "contain",
14988
15853
  alpha: "discard",
14989
15854
  bitrateMode: "variable",
14990
- latencyMode: VIDEO_LATENCY_MODE_REALTIME,
15855
+ latencyMode: config.latencyMode || VIDEO_LATENCY_MODE_REALTIME,
14991
15856
  contentHint: VIDEO_CONTENT_HINT_MOTION,
14992
15857
  hardwareAcceleration,
14993
- keyFrameInterval: keyFrameIntervalSeconds,
15858
+ keyFrameInterval: config.keyFrameInterval,
14994
15859
  bitrate: this.deserializeBitrate(config.bitrate)
14995
15860
  };
14996
15861
  this.videoSource = new VideoSampleSource(videoSourceOptions);
@@ -15017,7 +15882,13 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
15017
15882
  if (typeof config.bitrate === "number" && config.bitrate > 0) {
15018
15883
  bitrate = config.bitrate;
15019
15884
  }
15020
- return await resolveVideoHardwareAcceleration(config.codec, width, height, bitrate);
15885
+ const cacheKey = \`\${config.codec}:\${width}:\${height}:\${config.bitrate}\`;
15886
+ if (this.hwAccelCacheKey === cacheKey) {
15887
+ return this.resolvedHardwareAcceleration;
15888
+ }
15889
+ const result = await resolveVideoHardwareAcceleration(config.codec, width, height, bitrate);
15890
+ this.hwAccelCacheKey = cacheKey;
15891
+ return result;
15021
15892
  }
15022
15893
  setupAudioSource(audioConfig, config) {
15023
15894
  if (!audioConfig) {
@@ -15119,9 +15990,13 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
15119
15990
  format = DEFAULT_OUTPUT_FORMAT;
15120
15991
  }
15121
15992
  this.validateFormat(format);
15122
- this.resolvedHardwareAcceleration = await this.resolveHardwareAcceleration(config);
15123
- this.sendEncoderAcceleration(this.resolvedHardwareAcceleration);
15993
+ const hwAccelPromise = this.resolveHardwareAcceleration(config);
15124
15994
  this.createOutput();
15995
+ if (this.config?.watermark) {
15996
+ this.frameCompositor.prepareWatermark(this.config);
15997
+ }
15998
+ this.resolvedHardwareAcceleration = await hwAccelPromise;
15999
+ this.sendEncoderAcceleration(this.resolvedHardwareAcceleration);
15125
16000
  this.createVideoSource(config, this.resolvedHardwareAcceleration);
15126
16001
  if (videoStream) {
15127
16002
  this.setupVideoProcessingFromStream(videoStream);
@@ -15135,9 +16010,6 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
15135
16010
  this.setupAudioSource(audioConfig, config);
15136
16011
  }
15137
16012
  const output = requireNonNull(this.output, "Output must be initialized before starting");
15138
- if (this.config?.watermark) {
15139
- this.frameCompositor.prepareWatermark(this.config);
15140
- }
15141
16013
  await output.start();
15142
16014
  this.bufferTracker.start();
15143
16015
  this.sendReady();
@@ -15213,7 +16085,6 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
15213
16085
  const lastAudioTimestamp = this.audioState.getLastAudioTimestamp();
15214
16086
  const frameTiming = this.timestampManager.prepareFrameTiming({
15215
16087
  frameTimestamp,
15216
- keyFrameIntervalSeconds: config.keyFrameInterval,
15217
16088
  lastAudioTimestamp
15218
16089
  });
15219
16090
  const sample = new VideoSample(compositionResult.frameToProcess, {
@@ -15717,7 +16588,6 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
15717
16588
  this.expectedAudioChannels = null;
15718
16589
  this.expectedAudioSampleRate = null;
15719
16590
  this.pendingWriteCount = 0;
15720
- this.resolvedHardwareAcceleration = VIDEO_HARDWARE_ACCELERATION_PREFERENCE;
15721
16591
  this.consecutiveFrameErrors = 0;
15722
16592
  this.totalFrameErrors = 0;
15723
16593
  this.totalFramesProcessed = 0;
@@ -15941,6 +16811,7 @@ class WorkerProcessor {
15941
16811
  isPaused = false;
15942
16812
  overlayConfig = null;
15943
16813
  readyPromiseResolve = null;
16814
+ lastConfigFps = DEFAULT_SWITCH_SOURCE_FPS;
15944
16815
  workerProbeManager;
15945
16816
  canUseMainThreadVideoProcessorFn;
15946
16817
  createVideoStreamFromTrackFn;
@@ -16126,6 +16997,9 @@ class WorkerProcessor {
16126
16997
  bitrate: config.bitrate
16127
16998
  });
16128
16999
  const workerConfig = this.buildWorkerTranscodeConfig(config, audioCodec, audioBitrate, codec, format);
17000
+ if (typeof config.fps === "number" && config.fps > 0) {
17001
+ this.lastConfigFps = config.fps;
17002
+ }
16129
17003
  const videoTracks = stream.getVideoTracks();
16130
17004
  const audioTracks = stream.getAudioTracks();
16131
17005
  logger.debug("[WorkerProcessor] Preparing to start processing", {
@@ -16285,6 +17159,7 @@ class WorkerProcessor {
16285
17159
  audioBitrate,
16286
17160
  codec,
16287
17161
  keyFrameInterval: KEY_FRAME_INTERVAL_SECONDS,
17162
+ latencyMode: config.latencyMode,
16288
17163
  format,
16289
17164
  watermark: config.watermark
16290
17165
  };
@@ -16406,7 +17281,7 @@ class WorkerProcessor {
16406
17281
  return Promise.resolve();
16407
17282
  }
16408
17283
  const isScreenCapture = isScreenCaptureStream(newStream);
16409
- const targetFps = DEFAULT_SWITCH_SOURCE_FPS;
17284
+ const targetFps = this.lastConfigFps;
16410
17285
  logger.debug("[WorkerProcessor] Source type detected", {
16411
17286
  isScreenCapture,
16412
17287
  targetFps
@@ -16620,6 +17495,21 @@ class WorkerProcessor {
16620
17495
  this.totalSize = 0;
16621
17496
  return Promise.resolve();
16622
17497
  }
17498
+ warmupEncoder(config) {
17499
+ if (!this.worker) {
17500
+ return;
17501
+ }
17502
+ const format = config.format || "mp4";
17503
+ const policy = getFormatCompatibilityPolicy(format);
17504
+ const codec = config.codec || policy.preferredVideoCodec;
17505
+ const audioBitrate = config.audioBitrate !== undefined ? config.audioBitrate : policy.audioBitrate;
17506
+ const workerConfig = this.buildWorkerTranscodeConfig(config, policy.preferredAudioCodec, audioBitrate, codec, format);
17507
+ const message = {
17508
+ type: WORKER_MESSAGE_TYPE_WARMUP,
17509
+ config: workerConfig
17510
+ };
17511
+ this.worker.postMessage(message);
17512
+ }
16623
17513
  getBufferSize() {
16624
17514
  return this.totalSize;
16625
17515
  }
@@ -16932,6 +17822,9 @@ class StreamProcessor {
16932
17822
  setOnError(callback) {
16933
17823
  this.onError = callback;
16934
17824
  }
17825
+ warmupEncoder(config) {
17826
+ this.workerProcessor.warmupEncoder(config);
17827
+ }
16935
17828
  async cancel() {
16936
17829
  await this.workerProcessor.cancel();
16937
17830
  this.workerProcessor.cleanup();
@@ -17008,8 +17901,11 @@ class RecordingManager {
17008
17901
  getOriginalCameraStream() {
17009
17902
  return this.originalCameraStream;
17010
17903
  }
17011
- prewarmStreamProcessor() {
17012
- this.getOrCreateStreamProcessor();
17904
+ prewarmStreamProcessor(config) {
17905
+ const processor = this.getOrCreateStreamProcessor();
17906
+ if (config) {
17907
+ processor.warmupEncoder(config);
17908
+ }
17013
17909
  }
17014
17910
  async startRecording() {
17015
17911
  try {
@@ -17392,6 +18288,7 @@ class RecorderController {
17392
18288
  enableTabVisibilityOverlay = false;
17393
18289
  tabVisibilityOverlayText;
17394
18290
  recordingWarmupTimeoutId = null;
18291
+ audioTelemetryUnsub = null;
17395
18292
  constructor(callbacks = {}) {
17396
18293
  this.callbacks = callbacks;
17397
18294
  this.streamManager = new CameraStreamManager;
@@ -17443,6 +18340,14 @@ class RecorderController {
17443
18340
  }
17444
18341
  });
17445
18342
  }
18343
+ this.audioTelemetryUnsub = this.streamManager.on("audiotelemetry", ({ event }) => {
18344
+ const browserName = this.getBrowserNameForTelemetry();
18345
+ this.telemetryManager.sendEvent(event.name, {
18346
+ ...event.properties,
18347
+ browserName,
18348
+ sourceType: this.getCurrentSourceType()
18349
+ }, event.error);
18350
+ });
17446
18351
  }
17447
18352
  async initialize(config) {
17448
18353
  if (this.isInitialized) {
@@ -17485,7 +18390,9 @@ class RecorderController {
17485
18390
  logger.debug(`${LOGGER_PREFIX} startStream called`);
17486
18391
  await this.streamManager.startStream();
17487
18392
  this.ignorePromiseRejection(this.ensureConfigReady());
17488
- this.recordingManager.prewarmStreamProcessor();
18393
+ this.ignorePromiseRejection(this.configManager.getConfig().then((config) => {
18394
+ this.recordingManager.prewarmStreamProcessor(config);
18395
+ }));
17489
18396
  logger.debug(`${LOGGER_PREFIX} startStream completed`);
17490
18397
  },
17491
18398
  properties: {
@@ -17510,6 +18417,7 @@ class RecorderController {
17510
18417
  failedEvent: "recording.start.failed",
17511
18418
  action: async () => {
17512
18419
  await this.ensureConfigReady();
18420
+ await this.streamManager.waitForAudio();
17513
18421
  await this.recordingManager.startRecording();
17514
18422
  },
17515
18423
  properties: {
@@ -17656,6 +18564,10 @@ class RecorderController {
17656
18564
  clearTimeout(this.recordingWarmupTimeoutId);
17657
18565
  this.recordingWarmupTimeoutId = null;
17658
18566
  }
18567
+ if (this.audioTelemetryUnsub) {
18568
+ this.audioTelemetryUnsub();
18569
+ this.audioTelemetryUnsub = null;
18570
+ }
17659
18571
  this.uploadQueueManager?.destroy();
17660
18572
  this.storageManager.destroy();
17661
18573
  this.recordingManager.cleanup();
@@ -17695,6 +18607,19 @@ class RecorderController {
17695
18607
  isActive() {
17696
18608
  return this.streamManager.isActive();
17697
18609
  }
18610
+ isAudioReady() {
18611
+ return this.streamManager.isAudioReady();
18612
+ }
18613
+ getAudioStatus() {
18614
+ return this.streamManager.getAudioStatus();
18615
+ }
18616
+ getBrowserNameForTelemetry() {
18617
+ try {
18618
+ return getBrowserName();
18619
+ } catch {
18620
+ return "unknown";
18621
+ }
18622
+ }
17698
18623
  async initializeConfig(apiKey, backendUrl) {
17699
18624
  let shouldInitializeConfig = true;
17700
18625
  if (apiKey === null) {
@@ -17796,8 +18721,8 @@ class RecorderController {
17796
18721
  return;
17797
18722
  }
17798
18723
  this.ignorePromiseRejection(this.ensureConfigReady());
17799
- this.ignorePromiseRejection(Promise.resolve().then(() => {
17800
- this.recordingManager.prewarmStreamProcessor();
18724
+ this.ignorePromiseRejection(this.configManager.getConfig().then((config) => {
18725
+ this.recordingManager.prewarmStreamProcessor(config);
17801
18726
  }));
17802
18727
  }, RECORDING_WARMUP_DELAY_MILLISECONDS);
17803
18728
  }
@@ -17959,6 +18884,18 @@ function getCameraErrorText(errorCode, translations) {
17959
18884
  }
17960
18885
  return translations.failedToStartCamera;
17961
18886
  }
18887
+ function getAudioErrorText(errorCode, translations) {
18888
+ if (errorCode === "audio.in-use") {
18889
+ return translations.audioInUse;
18890
+ }
18891
+ if (errorCode === "audio.not-found") {
18892
+ return translations.audioNotFound;
18893
+ }
18894
+ if (errorCode === "audio.permission-denied") {
18895
+ return translations.audioPermissionDenied;
18896
+ }
18897
+ return translations.failedToStartAudio;
18898
+ }
17962
18899
  function formatDynamicBrowserUnsupportedText(template, browserName, browserVersion) {
17963
18900
  let resolvedBrowserName = FALLBACK_BROWSER_NAME;
17964
18901
  if (browserName && browserName.trim().length > 0) {
@@ -18012,12 +18949,13 @@ function parseBrowserErrorLinkContent(text) {
18012
18949
  // src/core/utils/device-detection.ts
18013
18950
  import { UAParser as UAParser3 } from "ua-parser-js";
18014
18951
  function isMobileDevice2() {
18015
- const parser = new UAParser3;
18952
+ const userAgent = globalThis.navigator && typeof globalThis.navigator.userAgent === "string" ? globalThis.navigator.userAgent : "";
18953
+ const parser = new UAParser3(userAgent);
18016
18954
  const result = parser.getResult();
18017
18955
  const deviceType = result.device.type;
18018
18956
  const isMobile = deviceType === "mobile" || deviceType === "tablet";
18019
18957
  logger.debug("Mobile detection result", {
18020
- userAgent: navigator.userAgent,
18958
+ userAgent,
18021
18959
  deviceType,
18022
18960
  isMobile,
18023
18961
  device: result.device,
@@ -18026,6 +18964,80 @@ function isMobileDevice2() {
18026
18964
  });
18027
18965
  return isMobile;
18028
18966
  }
18967
+ // src/core/utils/device-error-resolver.ts
18968
+ var ERROR_CODE_BROWSER_UNSUPPORTED2 = "browser.unsupported";
18969
+ var ERROR_CODE_CAMERA_PERMISSION_DENIED = "camera.permission-denied";
18970
+ var ERROR_CODE_AUDIO_PERMISSION_DENIED = "audio.permission-denied";
18971
+ var HIDDEN_RESULT = Object.freeze({
18972
+ visible: false,
18973
+ variant: "generic",
18974
+ canRetry: false,
18975
+ isCameraError: false,
18976
+ isAudioError: false,
18977
+ isBrowserUnsupported: false,
18978
+ isPermissionDenied: false
18979
+ });
18980
+ function resolveDeviceError(input) {
18981
+ const { errorCode, hasAudioFailed, error } = input;
18982
+ if (errorCode === ERROR_CODE_BROWSER_UNSUPPORTED2) {
18983
+ return {
18984
+ visible: true,
18985
+ variant: "browser",
18986
+ canRetry: false,
18987
+ isCameraError: false,
18988
+ isAudioError: false,
18989
+ isBrowserUnsupported: true,
18990
+ isPermissionDenied: false
18991
+ };
18992
+ }
18993
+ if (errorCode?.startsWith("camera.")) {
18994
+ const isPermissionDenied = errorCode === ERROR_CODE_CAMERA_PERMISSION_DENIED;
18995
+ return {
18996
+ visible: true,
18997
+ variant: "camera",
18998
+ canRetry: !isPermissionDenied,
18999
+ isCameraError: true,
19000
+ isAudioError: false,
19001
+ isBrowserUnsupported: false,
19002
+ isPermissionDenied
19003
+ };
19004
+ }
19005
+ if (hasAudioFailed) {
19006
+ return {
19007
+ visible: true,
19008
+ variant: "audio",
19009
+ canRetry: true,
19010
+ isCameraError: false,
19011
+ isAudioError: true,
19012
+ isBrowserUnsupported: false,
19013
+ isPermissionDenied: false
19014
+ };
19015
+ }
19016
+ if (errorCode?.startsWith("audio.")) {
19017
+ const isPermissionDenied = errorCode === ERROR_CODE_AUDIO_PERMISSION_DENIED;
19018
+ return {
19019
+ visible: true,
19020
+ variant: "audio",
19021
+ canRetry: !isPermissionDenied,
19022
+ isCameraError: false,
19023
+ isAudioError: true,
19024
+ isBrowserUnsupported: false,
19025
+ isPermissionDenied
19026
+ };
19027
+ }
19028
+ if (error) {
19029
+ return {
19030
+ visible: true,
19031
+ variant: "generic",
19032
+ canRetry: true,
19033
+ isCameraError: false,
19034
+ isAudioError: false,
19035
+ isBrowserUnsupported: false,
19036
+ isPermissionDenied: false
19037
+ };
19038
+ }
19039
+ return HIDDEN_RESULT;
19040
+ }
18029
19041
  // src/vidtreo-recorder.ts
18030
19042
  class VidtreoRecorder {
18031
19043
  controller;
@@ -18231,6 +19243,7 @@ export {
18231
19243
  validateBrowserSupport,
18232
19244
  transcodeVideoForNativeCamera,
18233
19245
  transcodeVideo,
19246
+ resolveDeviceError,
18234
19247
  requireStream,
18235
19248
  requireProcessor,
18236
19249
  requireNonNull,
@@ -18252,12 +19265,14 @@ export {
18252
19265
  getBrowserName,
18253
19266
  getBrowserInfo,
18254
19267
  getBrowserErrorText,
19268
+ getAudioErrorText,
18255
19269
  getAudioCodecForFormat,
18256
19270
  formatTime,
18257
19271
  formatFileSize,
18258
19272
  extractVideoDuration,
18259
19273
  extractLastFrame,
18260
19274
  extractErrorMessage,
19275
+ createPermissionFlowOrchestratorDependencies,
18261
19276
  checkRecorderSupport,
18262
19277
  calculateVideoBitrate,
18263
19278
  calculateTotalBitrateFromMbPerMinute,
@@ -18271,6 +19286,7 @@ export {
18271
19286
  RecordingManager,
18272
19287
  RecorderController,
18273
19288
  QuotaManager,
19289
+ PermissionFlowOrchestrator,
18274
19290
  PRESET_SIZE_LIMIT_MB_PER_MINUTE,
18275
19291
  NativeCameraHandler,
18276
19292
  FORMAT_DEFAULT_CODECS,