@vidtreo/recorder 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -65,6 +65,20 @@ export {};
65
65
 
66
66
  export {};
67
67
 
68
+ export {};
69
+
70
+ export {};
71
+
72
+ export {};
73
+
74
+ export {};
75
+
76
+ export {};
77
+
78
+ export {};
79
+
80
+ export {};
81
+
68
82
  export type VidtreoRecorderConfig = {
69
83
  apiKey: string;
70
84
  apiUrl?: string;
@@ -170,6 +184,7 @@ export declare class UploadQueueManager {
170
184
  private readonly processingIntervalId;
171
185
  private readonly networkOnlineHandler;
172
186
  private isProcessing;
187
+ private hasRecoveredStaleUploads;
173
188
  private retryTimeoutId;
174
189
  private callbacks;
175
190
  constructor(storageService: VideoStorageService, uploadService: VideoUploadService);
@@ -345,6 +360,15 @@ export declare function createBrowserUnsupportedError(options?: BrowserUnsupport
345
360
  export declare function validateBrowserSupport(): void;
346
361
  export {};
347
362
 
363
+ type PermissionName = "camera" | "microphone";
364
+ type PermissionStateChangeHandler = (state: PermissionState) => void;
365
+ export declare function queryPermissionState(permissionName: PermissionName): Promise<PermissionState>;
366
+ export declare function queryAllPermissions(): Promise<PermissionStatus>;
367
+ export declare function isPermissionGranted(state: PermissionState): boolean;
368
+ export declare function isPermissionBlocked(state: PermissionState): boolean;
369
+ export declare function createPermissionChangeListener(permissionName: PermissionName, onStateChange: PermissionStateChangeHandler): () => void;
370
+ export {};
371
+
348
372
  export type AvailableDevices = {
349
373
  audioinput: MediaDeviceInfo[];
350
374
  videoinput: MediaDeviceInfo[];
@@ -354,6 +378,41 @@ export type DeviceCallbacks = {
354
378
  onDeviceSelected: (type: "camera" | "mic", deviceId: string | null) => void;
355
379
  };
356
380
 
381
+ export declare function createPermissionFlowOrchestratorDependencies(): PermissionFlowOrchestratorDependencies;
382
+ export declare class PermissionFlowOrchestrator {
383
+ private readonly dependencies;
384
+ private readonly callbacks;
385
+ private readonly language;
386
+ private currentState;
387
+ private cleanupCameraListener;
388
+ private cleanupMicrophoneListener;
389
+ private isInitialized;
390
+ private hasCompleted;
391
+ constructor(dependencies: PermissionFlowOrchestratorDependencies, options?: PermissionFlowOrchestratorOptions);
392
+ initialize(): Promise<PermissionFlowState>;
393
+ requestCurrentStep(): Promise<PermissionFlowState>;
394
+ retryCurrentStep(): Promise<PermissionFlowState>;
395
+ getState(): PermissionFlowState;
396
+ private requestPermissionAndRefresh;
397
+ destroy(): void;
398
+ private setupPermissionListeners;
399
+ private handlePermissionStateChange;
400
+ private refreshPermissions;
401
+ private createStateFromPermissions;
402
+ private transitionToBlockedFromFailedProbe;
403
+ private createInsecureContextState;
404
+ private updateStateForChecking;
405
+ private emitChange;
406
+ private tryComplete;
407
+ }
408
+
409
+ export declare const DEVICE_PERSISTENCE_KEY_PREFIX = "vidtreo_device_prefs_";
410
+ export declare function createDevicePersistenceKey(apiKey: string): string;
411
+ export declare function saveDevicePreferences(apiKey: string, preferences: DevicePreferences): void;
412
+ export declare function loadDevicePreferences(apiKey: string): DevicePreferences | null;
413
+ export declare function clearDevicePreferences(apiKey: string): void;
414
+ export declare function validateSavedDeviceExists(savedDeviceId: string, availableDevices: MediaDeviceInfo[]): boolean;
415
+
357
416
  import type { CameraStreamManager } from "../stream/stream";
358
417
  export declare class DeviceManager {
359
418
  private readonly streamManager;
@@ -370,6 +429,96 @@ export declare class DeviceManager {
370
429
  getAvailableDevicesList(): AvailableDevices;
371
430
  }
372
431
 
432
+ export declare function createPermissionRecoveryData(errorCode: DeviceIssueCode, browserInfo: {
433
+ name: string;
434
+ version: string;
435
+ }, language?: "en" | "es"): PermissionRecoveryData;
436
+
437
+ export type PermissionState = "granted" | "denied" | "prompt" | "unknown";
438
+ export type PermissionStatus = {
439
+ camera: PermissionState;
440
+ microphone: PermissionState;
441
+ };
442
+ export type DeviceIssueCode = "permission-denied-camera" | "permission-denied-microphone" | "permission-denied-both" | "device-not-found" | "device-in-use" | "device-disconnected" | "silent-microphone" | "track-muted" | "track-ended" | "insecure-context";
443
+ export type DeviceIssue = {
444
+ code: DeviceIssueCode;
445
+ severity: "error" | "warning";
446
+ deviceType: "camera" | "microphone" | "both";
447
+ recoveryData?: PermissionRecoveryData;
448
+ };
449
+ export type PermissionRecoveryData = {
450
+ errorCode: DeviceIssueCode;
451
+ browserName: string;
452
+ browserVersion: string;
453
+ resetInstructions: string;
454
+ resetUrl?: string;
455
+ canRetry: boolean;
456
+ };
457
+ export type DevicePreflightResult = {
458
+ isReady: boolean;
459
+ permissions: PermissionStatus;
460
+ hasCamera: boolean;
461
+ hasMicrophone: boolean;
462
+ issues: DeviceIssue[];
463
+ };
464
+ export type DevicePreferences = {
465
+ cameraDeviceId?: string;
466
+ micDeviceId?: string;
467
+ };
468
+ export type DeviceHealthCallbacks = {
469
+ onDeviceDisconnected?: (deviceType: "camera" | "microphone", deviceId: string, fallbackDeviceId?: string) => void;
470
+ onDeviceFallback?: (requestedDeviceId: string, actualDeviceId: string, deviceType: "camera" | "microphone") => void;
471
+ onSilentMicDetected?: () => void;
472
+ onSilentMicRecovered?: () => void;
473
+ onTrackHealthChange?: (trackType: "audio" | "video", event: "muted" | "unmuted" | "ended") => void;
474
+ onPermissionStateChange?: (permissions: PermissionStatus) => void;
475
+ onPreflightComplete?: (result: DevicePreflightResult) => void;
476
+ };
477
+ export type DeviceAccessConfig = {
478
+ enableDevicePersistence?: boolean;
479
+ enableSilentMicDetection?: boolean;
480
+ enableTrackHealthMonitoring?: boolean;
481
+ silentMicThresholdLevel?: number;
482
+ silentMicThresholdDuration?: number;
483
+ deviceChangeDebounceMs?: number;
484
+ };
485
+ export type PermissionFlowStep = "idle" | "checking" | "awaiting-user" | "blocked" | "ready";
486
+ export type PermissionDenialType = "none" | "soft" | "hard";
487
+ export type PermissionFlowChangeReason = "initialized" | "permission-refresh" | "permission-change" | "current-step-requested" | "retry-requested" | "probe-complete" | "insecure-context" | "destroyed";
488
+ export type PermissionFlowSnapshot = {
489
+ step: PermissionFlowStep;
490
+ permissions: PermissionStatus;
491
+ denialType: PermissionDenialType;
492
+ issueCode?: DeviceIssueCode;
493
+ recoveryData?: PermissionRecoveryData;
494
+ isSecureContext: boolean;
495
+ isComplete: boolean;
496
+ canRetry: boolean;
497
+ shouldProbeUnknown: boolean;
498
+ };
499
+ export type PermissionFlowState = PermissionFlowSnapshot;
500
+ export type PermissionFlowCallbacks = {
501
+ onChange?: (state: PermissionFlowState, reason: PermissionFlowChangeReason) => void;
502
+ onComplete?: (state: PermissionFlowState) => void;
503
+ };
504
+ export type PermissionFlowLanguage = "en" | "es";
505
+ export type PermissionFlowBrowserInfo = {
506
+ name: string;
507
+ version: string;
508
+ };
509
+ export type PermissionFlowOrchestratorDependencies = {
510
+ queryAllPermissions: () => Promise<PermissionStatus>;
511
+ createPermissionChangeListener: (permissionName: "camera" | "microphone", onStateChange: (state: PermissionState) => void) => () => void;
512
+ createPermissionRecoveryData: (errorCode: DeviceIssueCode, browserInfo: PermissionFlowBrowserInfo, language?: PermissionFlowLanguage) => PermissionRecoveryData;
513
+ getBrowserInfo: () => PermissionFlowBrowserInfo;
514
+ getIsSecureContext: () => boolean;
515
+ requestPermissionProbe: () => Promise<void>;
516
+ };
517
+ export type PermissionFlowOrchestratorOptions = {
518
+ callbacks?: PermissionFlowCallbacks;
519
+ language?: PermissionFlowLanguage;
520
+ };
521
+
373
522
  export type NativeCameraFile = {
374
523
  file: File;
375
524
  previewUrl: string;
package/dist/index.js CHANGED
@@ -704,6 +704,488 @@ class DeviceManager {
704
704
  return this.availableDevices;
705
705
  }
706
706
  }
707
+ // src/core/device/permission-checker.ts
708
+ var noOperation = () => {
709
+ return;
710
+ };
711
+ function isPermissionsApiAvailable() {
712
+ if (typeof navigator === "undefined") {
713
+ return false;
714
+ }
715
+ if (!navigator.permissions) {
716
+ return false;
717
+ }
718
+ return typeof navigator.permissions.query === "function";
719
+ }
720
+ function queryPermissionState(permissionName) {
721
+ if (!isPermissionsApiAvailable()) {
722
+ return Promise.resolve("unknown");
723
+ }
724
+ return navigator.permissions.query({ name: permissionName }).then((status) => status.state).catch(() => "unknown");
725
+ }
726
+ async function queryAllPermissions() {
727
+ const [camera, microphone] = await Promise.all([
728
+ queryPermissionState("camera"),
729
+ queryPermissionState("microphone")
730
+ ]);
731
+ return { camera, microphone };
732
+ }
733
+ function createPermissionChangeListener(permissionName, onStateChange) {
734
+ if (!isPermissionsApiAvailable()) {
735
+ return noOperation;
736
+ }
737
+ let permissionStatus = null;
738
+ let isDestroyed = false;
739
+ const handleChange = () => {
740
+ if (!permissionStatus) {
741
+ return;
742
+ }
743
+ onStateChange(permissionStatus.state);
744
+ };
745
+ navigator.permissions.query({ name: permissionName }).then((status) => {
746
+ if (isDestroyed) {
747
+ return;
748
+ }
749
+ permissionStatus = status;
750
+ permissionStatus.addEventListener("change", handleChange);
751
+ }).catch(noOperation);
752
+ return () => {
753
+ isDestroyed = true;
754
+ if (permissionStatus) {
755
+ permissionStatus.removeEventListener("change", handleChange);
756
+ permissionStatus = null;
757
+ }
758
+ };
759
+ }
760
+
761
+ // src/core/device/permission-recovery.ts
762
+ var CHROME_INSTRUCTIONS = {
763
+ en: "Click the lock/tune icon in the address bar → Site settings → Camera/Microphone → Allow",
764
+ es: "Haz clic en el icono de candado/ajustes en la barra de direcciones → Configuración del sitio → Cámara/Micrófono → Permitir",
765
+ resetUrl: "chrome://settings/content/camera"
766
+ };
767
+ var SAFARI_INSTRUCTIONS = {
768
+ en: "Go to Safari → Settings → Websites → Camera/Microphone → Allow for this site",
769
+ es: "Ve a Safari → Ajustes → Sitios web → Cámara/Micrófono → Permitir para este sitio"
770
+ };
771
+ var EDGE_INSTRUCTIONS = {
772
+ en: "Click the lock icon in the address bar → Permissions → Camera/Microphone → Allow",
773
+ es: "Haz clic en el icono de candado en la barra de direcciones → Permisos → Cámara/Micrófono → Permitir",
774
+ resetUrl: "edge://settings/content/camera"
775
+ };
776
+ var OPERA_INSTRUCTIONS = {
777
+ en: "Click the lock icon → Site settings → Camera/Microphone → Allow",
778
+ es: "Haz clic en el icono de candado → Configuración del sitio → Cámara/Micrófono → Permitir",
779
+ resetUrl: "opera://settings/content/camera"
780
+ };
781
+ var UNKNOWN_BROWSER_INSTRUCTIONS = {
782
+ en: "Please check your browser settings to allow camera and microphone access for this site",
783
+ 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"
784
+ };
785
+ var INSECURE_CONTEXT_INSTRUCTIONS = {
786
+ en: "This site must be accessed over HTTPS (secure connection) to use camera and microphone",
787
+ es: "Este sitio debe ser accedido a través de HTTPS (conexión segura) para usar cámara y micrófono"
788
+ };
789
+ var BROWSER_INSTRUCTIONS_MAP = {
790
+ chrome: CHROME_INSTRUCTIONS,
791
+ safari: SAFARI_INSTRUCTIONS,
792
+ edge: EDGE_INSTRUCTIONS,
793
+ opera: OPERA_INSTRUCTIONS,
794
+ unknown: UNKNOWN_BROWSER_INSTRUCTIONS
795
+ };
796
+ var RETRYABLE_ERROR_CODES = new Set([
797
+ "permission-denied-camera",
798
+ "permission-denied-microphone",
799
+ "permission-denied-both",
800
+ "device-not-found",
801
+ "device-in-use"
802
+ ]);
803
+ function resolveBrowserKey(browserName) {
804
+ const normalizedName = browserName.toLowerCase();
805
+ if (normalizedName.includes("edge")) {
806
+ return "edge";
807
+ }
808
+ if (normalizedName.includes("opera") || normalizedName.includes("opr")) {
809
+ return "opera";
810
+ }
811
+ if (normalizedName.includes("chrome")) {
812
+ return "chrome";
813
+ }
814
+ if (normalizedName.includes("safari")) {
815
+ return "safari";
816
+ }
817
+ return "unknown";
818
+ }
819
+ function createPermissionRecoveryData(errorCode, browserInfo, language = "en") {
820
+ if (errorCode === "insecure-context") {
821
+ return {
822
+ errorCode,
823
+ browserName: browserInfo.name,
824
+ browserVersion: browserInfo.version,
825
+ resetInstructions: INSECURE_CONTEXT_INSTRUCTIONS[language],
826
+ resetUrl: undefined,
827
+ canRetry: false
828
+ };
829
+ }
830
+ const browserKey = resolveBrowserKey(browserInfo.name);
831
+ const instructions = BROWSER_INSTRUCTIONS_MAP[browserKey];
832
+ const canRetry = RETRYABLE_ERROR_CODES.has(errorCode);
833
+ return {
834
+ errorCode,
835
+ browserName: browserInfo.name,
836
+ browserVersion: browserInfo.version,
837
+ resetInstructions: instructions[language],
838
+ resetUrl: instructions.resetUrl,
839
+ canRetry
840
+ };
841
+ }
842
+
843
+ // src/core/device/permission-flow-orchestrator.ts
844
+ var GRANTED_PERMISSION_COUNT = 2;
845
+ var DEFAULT_PERMISSION_STATUS = {
846
+ camera: "unknown",
847
+ microphone: "unknown"
848
+ };
849
+ function noOperation2() {
850
+ return;
851
+ }
852
+ function getIsSecureContext() {
853
+ const globalWindow = globalThis.window;
854
+ if (!globalWindow) {
855
+ return false;
856
+ }
857
+ return globalWindow.isSecureContext;
858
+ }
859
+ function stopStreamTracks(stream) {
860
+ const tracks = stream.getTracks();
861
+ for (const track of tracks) {
862
+ track.stop();
863
+ }
864
+ }
865
+ async function requestPermissionProbe() {
866
+ const mediaDevices = globalThis.navigator?.mediaDevices;
867
+ if (!mediaDevices) {
868
+ return;
869
+ }
870
+ const canGetUserMedia = typeof mediaDevices.getUserMedia === "function";
871
+ if (!canGetUserMedia) {
872
+ return;
873
+ }
874
+ const stream = await mediaDevices.getUserMedia({ audio: true, video: true });
875
+ stopStreamTracks(stream);
876
+ await mediaDevices.getUserMedia({
877
+ audio: true,
878
+ video: { width: { ideal: 1920 }, height: { ideal: 1080 } }
879
+ }).then((stream2) => {
880
+ stopStreamTracks(stream2);
881
+ return;
882
+ }).catch(noOperation2);
883
+ }
884
+ function countGrantedPermissions(permissions) {
885
+ let grantedCount = 0;
886
+ if (permissions.camera === "granted") {
887
+ grantedCount += 1;
888
+ }
889
+ if (permissions.microphone === "granted") {
890
+ grantedCount += 1;
891
+ }
892
+ return grantedCount;
893
+ }
894
+ function hasUnknownPermissionState(permissions) {
895
+ if (permissions.camera === "unknown") {
896
+ return true;
897
+ }
898
+ if (permissions.microphone === "unknown") {
899
+ return true;
900
+ }
901
+ return false;
902
+ }
903
+ function hasPromptPermissionState(permissions) {
904
+ if (permissions.camera === "prompt") {
905
+ return true;
906
+ }
907
+ if (permissions.microphone === "prompt") {
908
+ return true;
909
+ }
910
+ return false;
911
+ }
912
+ function hasHardPermissionDenial(permissions) {
913
+ if (permissions.camera === "denied") {
914
+ return true;
915
+ }
916
+ if (permissions.microphone === "denied") {
917
+ return true;
918
+ }
919
+ return false;
920
+ }
921
+ function getPermissionDeniedIssueCode(permissions) {
922
+ const hasCameraDenied = permissions.camera === "denied";
923
+ const hasMicrophoneDenied = permissions.microphone === "denied";
924
+ if (hasCameraDenied && hasMicrophoneDenied) {
925
+ return "permission-denied-both";
926
+ }
927
+ if (hasCameraDenied) {
928
+ return "permission-denied-camera";
929
+ }
930
+ return "permission-denied-microphone";
931
+ }
932
+ function createPermissionFlowOrchestratorDependencies() {
933
+ return {
934
+ queryAllPermissions,
935
+ createPermissionChangeListener,
936
+ createPermissionRecoveryData,
937
+ getBrowserInfo: () => {
938
+ const browserInfo = getBrowserInfo();
939
+ return {
940
+ name: browserInfo.name,
941
+ version: browserInfo.version
942
+ };
943
+ },
944
+ getIsSecureContext,
945
+ requestPermissionProbe
946
+ };
947
+ }
948
+
949
+ class PermissionFlowOrchestrator {
950
+ dependencies;
951
+ callbacks;
952
+ language;
953
+ currentState;
954
+ cleanupCameraListener = noOperation2;
955
+ cleanupMicrophoneListener = noOperation2;
956
+ isInitialized = false;
957
+ hasCompleted = false;
958
+ constructor(dependencies, options = {}) {
959
+ this.dependencies = dependencies;
960
+ let callbacks = {};
961
+ if (options.callbacks) {
962
+ callbacks = options.callbacks;
963
+ }
964
+ this.callbacks = callbacks;
965
+ let language = "en";
966
+ if (options.language === "es") {
967
+ language = "es";
968
+ }
969
+ this.language = language;
970
+ this.currentState = {
971
+ step: "idle",
972
+ permissions: DEFAULT_PERMISSION_STATUS,
973
+ denialType: "none",
974
+ isSecureContext: true,
975
+ isComplete: false,
976
+ canRetry: true,
977
+ shouldProbeUnknown: true
978
+ };
979
+ }
980
+ initialize() {
981
+ if (this.isInitialized) {
982
+ return Promise.resolve(this.currentState);
983
+ }
984
+ this.isInitialized = true;
985
+ this.hasCompleted = false;
986
+ this.setupPermissionListeners();
987
+ this.updateStateForChecking();
988
+ this.emitChange("initialized");
989
+ const isSecureContext = this.dependencies.getIsSecureContext();
990
+ if (!isSecureContext) {
991
+ this.currentState = this.createInsecureContextState();
992
+ this.emitChange("insecure-context");
993
+ this.tryComplete();
994
+ return Promise.resolve(this.currentState);
995
+ }
996
+ return this.refreshPermissions("permission-refresh");
997
+ }
998
+ requestCurrentStep() {
999
+ if (!this.isInitialized) {
1000
+ return this.initialize();
1001
+ }
1002
+ if (!this.currentState.canRetry) {
1003
+ return Promise.resolve(this.currentState);
1004
+ }
1005
+ this.updateStateForChecking();
1006
+ this.emitChange("current-step-requested");
1007
+ return this.requestPermissionAndRefresh();
1008
+ }
1009
+ retryCurrentStep() {
1010
+ if (!this.isInitialized) {
1011
+ return this.initialize();
1012
+ }
1013
+ if (!this.currentState.canRetry) {
1014
+ return Promise.resolve(this.currentState);
1015
+ }
1016
+ this.updateStateForChecking();
1017
+ this.emitChange("retry-requested");
1018
+ return this.requestPermissionAndRefresh();
1019
+ }
1020
+ getState() {
1021
+ return this.currentState;
1022
+ }
1023
+ requestPermissionAndRefresh() {
1024
+ return this.dependencies.requestPermissionProbe().then(() => {
1025
+ const grantedPermissions = {
1026
+ camera: "granted",
1027
+ microphone: "granted"
1028
+ };
1029
+ this.currentState = this.createStateFromPermissions(grantedPermissions);
1030
+ this.emitChange("probe-complete");
1031
+ this.tryComplete();
1032
+ return this.currentState;
1033
+ }).catch(() => this.refreshPermissions("permission-refresh").then((state) => {
1034
+ if (state.step !== "awaiting-user") {
1035
+ return state;
1036
+ }
1037
+ return this.transitionToBlockedFromFailedProbe();
1038
+ }));
1039
+ }
1040
+ destroy() {
1041
+ this.cleanupCameraListener();
1042
+ this.cleanupCameraListener = noOperation2;
1043
+ this.cleanupMicrophoneListener();
1044
+ this.cleanupMicrophoneListener = noOperation2;
1045
+ this.isInitialized = false;
1046
+ this.hasCompleted = false;
1047
+ this.emitChange("destroyed");
1048
+ }
1049
+ setupPermissionListeners() {
1050
+ this.cleanupCameraListener = this.dependencies.createPermissionChangeListener("camera", (state) => {
1051
+ this.handlePermissionStateChange("camera", state);
1052
+ });
1053
+ this.cleanupMicrophoneListener = this.dependencies.createPermissionChangeListener("microphone", (state) => {
1054
+ this.handlePermissionStateChange("microphone", state);
1055
+ });
1056
+ }
1057
+ handlePermissionStateChange(permissionName, state) {
1058
+ if (!this.isInitialized) {
1059
+ return;
1060
+ }
1061
+ const nextPermissions = {
1062
+ ...this.currentState.permissions,
1063
+ [permissionName]: state
1064
+ };
1065
+ const previousStep = this.currentState.step;
1066
+ this.currentState = this.createStateFromPermissions(nextPermissions);
1067
+ const isFlowReactivating = previousStep === "ready" && this.currentState.step !== "ready";
1068
+ if (isFlowReactivating) {
1069
+ this.hasCompleted = false;
1070
+ }
1071
+ this.emitChange("permission-change");
1072
+ this.tryComplete();
1073
+ }
1074
+ async refreshPermissions(reason) {
1075
+ const permissions = await this.dependencies.queryAllPermissions();
1076
+ this.currentState = this.createStateFromPermissions(permissions);
1077
+ this.emitChange(reason);
1078
+ this.tryComplete();
1079
+ return this.currentState;
1080
+ }
1081
+ createStateFromPermissions(permissions) {
1082
+ const isSecureContext = this.dependencies.getIsSecureContext();
1083
+ if (!isSecureContext) {
1084
+ return this.createInsecureContextState();
1085
+ }
1086
+ const grantedCount = countGrantedPermissions(permissions);
1087
+ if (grantedCount === GRANTED_PERMISSION_COUNT) {
1088
+ return {
1089
+ step: "ready",
1090
+ permissions,
1091
+ denialType: "none",
1092
+ isSecureContext: true,
1093
+ isComplete: true,
1094
+ canRetry: false,
1095
+ shouldProbeUnknown: false
1096
+ };
1097
+ }
1098
+ if (hasHardPermissionDenial(permissions)) {
1099
+ const issueCode = getPermissionDeniedIssueCode(permissions);
1100
+ const recoveryData = this.dependencies.createPermissionRecoveryData(issueCode, this.dependencies.getBrowserInfo(), this.language);
1101
+ return {
1102
+ step: "blocked",
1103
+ permissions,
1104
+ denialType: "hard",
1105
+ issueCode,
1106
+ recoveryData,
1107
+ isSecureContext: true,
1108
+ isComplete: true,
1109
+ canRetry: recoveryData.canRetry,
1110
+ shouldProbeUnknown: false
1111
+ };
1112
+ }
1113
+ const shouldProbeUnknown = hasUnknownPermissionState(permissions);
1114
+ const hasPromptState = hasPromptPermissionState(permissions);
1115
+ let canRetry = false;
1116
+ if (shouldProbeUnknown) {
1117
+ canRetry = true;
1118
+ }
1119
+ if (hasPromptState) {
1120
+ canRetry = true;
1121
+ }
1122
+ return {
1123
+ step: "awaiting-user",
1124
+ permissions,
1125
+ denialType: "soft",
1126
+ isSecureContext: true,
1127
+ isComplete: false,
1128
+ canRetry,
1129
+ shouldProbeUnknown
1130
+ };
1131
+ }
1132
+ transitionToBlockedFromFailedProbe() {
1133
+ const issueCode = "permission-denied-both";
1134
+ const recoveryData = this.dependencies.createPermissionRecoveryData(issueCode, this.dependencies.getBrowserInfo(), this.language);
1135
+ this.currentState = {
1136
+ step: "blocked",
1137
+ permissions: this.currentState.permissions,
1138
+ denialType: "hard",
1139
+ issueCode,
1140
+ recoveryData,
1141
+ isSecureContext: true,
1142
+ isComplete: true,
1143
+ canRetry: recoveryData.canRetry,
1144
+ shouldProbeUnknown: false
1145
+ };
1146
+ this.emitChange("probe-complete");
1147
+ this.tryComplete();
1148
+ return this.currentState;
1149
+ }
1150
+ createInsecureContextState() {
1151
+ const issueCode = "insecure-context";
1152
+ const recoveryData = this.dependencies.createPermissionRecoveryData(issueCode, this.dependencies.getBrowserInfo(), this.language);
1153
+ return {
1154
+ step: "blocked",
1155
+ permissions: this.currentState.permissions,
1156
+ denialType: "hard",
1157
+ issueCode,
1158
+ recoveryData,
1159
+ isSecureContext: false,
1160
+ isComplete: true,
1161
+ canRetry: false,
1162
+ shouldProbeUnknown: false
1163
+ };
1164
+ }
1165
+ updateStateForChecking() {
1166
+ this.currentState = {
1167
+ ...this.currentState,
1168
+ step: "checking"
1169
+ };
1170
+ }
1171
+ emitChange(reason) {
1172
+ if (this.callbacks.onChange) {
1173
+ this.callbacks.onChange(this.currentState, reason);
1174
+ }
1175
+ }
1176
+ tryComplete() {
1177
+ if (this.hasCompleted) {
1178
+ return;
1179
+ }
1180
+ if (!this.currentState.isComplete) {
1181
+ return;
1182
+ }
1183
+ this.hasCompleted = true;
1184
+ if (this.callbacks.onComplete) {
1185
+ this.callbacks.onComplete(this.currentState);
1186
+ }
1187
+ }
1188
+ }
707
1189
  // src/core/utils/video-utils.ts
708
1190
  import { BlobSource, Input, MP4 } from "mediabunny";
709
1191
  async function extractVideoDuration(file) {
@@ -1845,7 +2327,8 @@ class VideoStorageService {
1845
2327
  reject(new Error("Upload not found"));
1846
2328
  return;
1847
2329
  }
1848
- const updated = { ...upload, ...updates, updatedAt: Date.now() };
2330
+ const updatedAt = updates.updatedAt !== undefined ? updates.updatedAt : Date.now();
2331
+ const updated = { ...upload, ...updates, updatedAt };
1849
2332
  const putRequest = store.put(updated);
1850
2333
  putRequest.onsuccess = () => resolve();
1851
2334
  putRequest.onerror = () => {
@@ -2135,7 +2618,7 @@ function stopLiveTracks(tracks) {
2135
2618
  }
2136
2619
  }
2137
2620
  }
2138
- function stopStreamTracks(stream) {
2621
+ function stopStreamTracks2(stream) {
2139
2622
  stopLiveTracks(stream.getTracks());
2140
2623
  }
2141
2624
  function stopStreamVideoTracks(stream) {
@@ -2152,7 +2635,7 @@ function areTracksLive(videoTrack, audioTrack) {
2152
2635
  }
2153
2636
  function validateTrack(track, trackType, stream) {
2154
2637
  if (!isTrackLive(track)) {
2155
- stopStreamTracks(stream);
2638
+ stopStreamTracks2(stream);
2156
2639
  let readyState = "undefined";
2157
2640
  if (track) {
2158
2641
  readyState = track.readyState;
@@ -2310,7 +2793,7 @@ class CameraStreamBuilder {
2310
2793
  }
2311
2794
  const managerStream = this.dependencies.streamManager.getStream();
2312
2795
  if (!isRecording && managerStream && managerStream !== this.dependencies.getOriginalCameraStream()) {
2313
- stopStreamTracks(managerStream);
2796
+ stopStreamTracks2(managerStream);
2314
2797
  this.dependencies.streamManager.setMediaStream(null);
2315
2798
  }
2316
2799
  if (isRecording) {
@@ -2638,7 +3121,7 @@ class SourceSwitchManager {
2638
3121
  callbacks: this.callbacks,
2639
3122
  streamManager: this.streamManager,
2640
3123
  combineScreenShareWithOriginalAudio: (screenVideoTrack) => this.combineScreenShareWithOriginalAudio(screenVideoTrack),
2641
- stopStreamTracks: (stream) => stopStreamTracks(stream),
3124
+ stopStreamTracks: (stream) => stopStreamTracks2(stream),
2642
3125
  stopStreamVideoTracks: (stream) => stopStreamVideoTracks(stream),
2643
3126
  getCurrentSourceType: () => this.currentSourceType,
2644
3127
  setCurrentSourceType: (sourceType) => {
@@ -3908,7 +4391,7 @@ class CameraStreamManager {
3908
4391
  // package.json
3909
4392
  var package_default = {
3910
4393
  name: "@vidtreo/recorder",
3911
- version: "1.3.3",
4394
+ version: "1.4.0",
3912
4395
  type: "module",
3913
4396
  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.",
3914
4397
  main: "./dist/index.js",
@@ -3957,6 +4440,7 @@ var package_default = {
3957
4440
  devDependencies: {
3958
4441
  "@happy-dom/global-registrator": "^20.6.0",
3959
4442
  "@types/node": "^25.2.3",
4443
+ "fake-indexeddb": "^6.2.5",
3960
4444
  typescript: "^5.9.3",
3961
4445
  vite: "^7.3.1"
3962
4446
  },
@@ -4456,6 +4940,7 @@ class UploadQueueManager {
4456
4940
  processingIntervalId;
4457
4941
  networkOnlineHandler;
4458
4942
  isProcessing = false;
4943
+ hasRecoveredStaleUploads = false;
4459
4944
  retryTimeoutId = null;
4460
4945
  callbacks = {};
4461
4946
  constructor(storageService, uploadService) {
@@ -4485,7 +4970,10 @@ class UploadQueueManager {
4485
4970
  }
4486
4971
  async queueUpload(upload) {
4487
4972
  const id = await this.storageService.savePendingUpload(upload);
4488
- this.processQueue();
4973
+ this.processQueue().catch((error) => {
4974
+ const errorMessage = extractErrorMessage(error);
4975
+ this.callbacks.onUploadError?.(id, new Error(errorMessage));
4976
+ });
4489
4977
  return id;
4490
4978
  }
4491
4979
  async processQueue() {
@@ -4497,6 +4985,13 @@ class UploadQueueManager {
4497
4985
  if (!this.storageService.isInitialized()) {
4498
4986
  throw new Error("Database not initialized");
4499
4987
  }
4988
+ if (!this.hasRecoveredStaleUploads) {
4989
+ const staleUploads = await this.storageService.getPendingUploads("uploading");
4990
+ await Promise.all(staleUploads.map((upload) => this.storageService.updateUploadStatus(upload.id, {
4991
+ status: "pending"
4992
+ })));
4993
+ this.hasRecoveredStaleUploads = true;
4994
+ }
4500
4995
  const pendingUploads = await this.storageService.getPendingUploads("pending");
4501
4996
  if (pendingUploads.length > 0) {
4502
4997
  const upload = this.getOldestUpload(pendingUploads);
@@ -4602,7 +5097,10 @@ class UploadQueueManager {
4602
5097
  this.clearTimer(this.retryTimeoutId, clearTimeout);
4603
5098
  this.retryTimeoutId = window.setTimeout(() => {
4604
5099
  this.retryTimeoutId = null;
4605
- this.processQueue();
5100
+ this.processQueue().catch((error) => {
5101
+ const errorMessage = extractErrorMessage(error);
5102
+ this.callbacks.onUploadError?.("scheduled-retry", new Error(errorMessage));
5103
+ });
4606
5104
  }, delay);
4607
5105
  }
4608
5106
  clearTimer(timerId, clearFn) {
@@ -17640,6 +18138,7 @@ class RecorderController {
17640
18138
  clearTimeout(this.recordingWarmupTimeoutId);
17641
18139
  this.recordingWarmupTimeoutId = null;
17642
18140
  }
18141
+ this.uploadQueueManager?.destroy();
17643
18142
  this.storageManager.destroy();
17644
18143
  this.recordingManager.cleanup();
17645
18144
  this.audioLevelAnalyzer.stopTracking();
@@ -17706,8 +18205,14 @@ class RecorderController {
17706
18205
  this.recordingManager.setTabVisibilityOverlayConfig(this.enableTabVisibilityOverlay, this.tabVisibilityOverlayText);
17707
18206
  }
17708
18207
  async initializeStorage() {
18208
+ if (this.isDestroyed) {
18209
+ return;
18210
+ }
17709
18211
  const onStorageCleanupError = resolveStorageCleanupErrorCallback(this.callbacks);
17710
18212
  await this.storageManager.initialize(onStorageCleanupError);
18213
+ if (this.isDestroyed) {
18214
+ return;
18215
+ }
17711
18216
  const storageService = this.storageManager.getStorageService();
17712
18217
  if (!(storageService && this.uploadService)) {
17713
18218
  return;
@@ -18235,6 +18740,7 @@ export {
18235
18740
  extractVideoDuration,
18236
18741
  extractLastFrame,
18237
18742
  extractErrorMessage,
18743
+ createPermissionFlowOrchestratorDependencies,
18238
18744
  checkRecorderSupport,
18239
18745
  calculateVideoBitrate,
18240
18746
  calculateTotalBitrateFromMbPerMinute,
@@ -18248,6 +18754,7 @@ export {
18248
18754
  RecordingManager,
18249
18755
  RecorderController,
18250
18756
  QuotaManager,
18757
+ PermissionFlowOrchestrator,
18251
18758
  PRESET_SIZE_LIMIT_MB_PER_MINUTE,
18252
18759
  NativeCameraHandler,
18253
18760
  FORMAT_DEFAULT_CODECS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidtreo/recorder",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "description": "Vidtreo SDK for browser-based video recording and transcoding. Features include camera/screen recording, real-time MP4 transcoding, audio level analysis, mute/pause controls, source switching, device selection, and automatic backend uploads. Similar to Ziggeo and Addpipe, Vidtreo provides enterprise-grade video processing capabilities for web applications.",
6
6
  "main": "./dist/index.js",
@@ -49,6 +49,7 @@
49
49
  "devDependencies": {
50
50
  "@happy-dom/global-registrator": "^20.6.0",
51
51
  "@types/node": "^25.2.3",
52
+ "fake-indexeddb": "^6.2.5",
52
53
  "typescript": "^5.9.3",
53
54
  "vite": "^7.3.1"
54
55
  },