@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 +149 -0
- package/dist/index.js +515 -8
- package/package.json +2 -1
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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.
|
|
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
|
+
"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
|
},
|