@vidtreo/recorder 1.3.4 → 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 +144 -0
- package/dist/index.js +489 -5
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -69,6 +69,16 @@ export {};
|
|
|
69
69
|
|
|
70
70
|
export {};
|
|
71
71
|
|
|
72
|
+
export {};
|
|
73
|
+
|
|
74
|
+
export {};
|
|
75
|
+
|
|
76
|
+
export {};
|
|
77
|
+
|
|
78
|
+
export {};
|
|
79
|
+
|
|
80
|
+
export {};
|
|
81
|
+
|
|
72
82
|
export type VidtreoRecorderConfig = {
|
|
73
83
|
apiKey: string;
|
|
74
84
|
apiUrl?: string;
|
|
@@ -350,6 +360,15 @@ export declare function createBrowserUnsupportedError(options?: BrowserUnsupport
|
|
|
350
360
|
export declare function validateBrowserSupport(): void;
|
|
351
361
|
export {};
|
|
352
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
|
+
|
|
353
372
|
export type AvailableDevices = {
|
|
354
373
|
audioinput: MediaDeviceInfo[];
|
|
355
374
|
videoinput: MediaDeviceInfo[];
|
|
@@ -359,6 +378,41 @@ export type DeviceCallbacks = {
|
|
|
359
378
|
onDeviceSelected: (type: "camera" | "mic", deviceId: string | null) => void;
|
|
360
379
|
};
|
|
361
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
|
+
|
|
362
416
|
import type { CameraStreamManager } from "../stream/stream";
|
|
363
417
|
export declare class DeviceManager {
|
|
364
418
|
private readonly streamManager;
|
|
@@ -375,6 +429,96 @@ export declare class DeviceManager {
|
|
|
375
429
|
getAvailableDevicesList(): AvailableDevices;
|
|
376
430
|
}
|
|
377
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
|
+
|
|
378
522
|
export type NativeCameraFile = {
|
|
379
523
|
file: File;
|
|
380
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) {
|
|
@@ -2136,7 +2618,7 @@ function stopLiveTracks(tracks) {
|
|
|
2136
2618
|
}
|
|
2137
2619
|
}
|
|
2138
2620
|
}
|
|
2139
|
-
function
|
|
2621
|
+
function stopStreamTracks2(stream) {
|
|
2140
2622
|
stopLiveTracks(stream.getTracks());
|
|
2141
2623
|
}
|
|
2142
2624
|
function stopStreamVideoTracks(stream) {
|
|
@@ -2153,7 +2635,7 @@ function areTracksLive(videoTrack, audioTrack) {
|
|
|
2153
2635
|
}
|
|
2154
2636
|
function validateTrack(track, trackType, stream) {
|
|
2155
2637
|
if (!isTrackLive(track)) {
|
|
2156
|
-
|
|
2638
|
+
stopStreamTracks2(stream);
|
|
2157
2639
|
let readyState = "undefined";
|
|
2158
2640
|
if (track) {
|
|
2159
2641
|
readyState = track.readyState;
|
|
@@ -2311,7 +2793,7 @@ class CameraStreamBuilder {
|
|
|
2311
2793
|
}
|
|
2312
2794
|
const managerStream = this.dependencies.streamManager.getStream();
|
|
2313
2795
|
if (!isRecording && managerStream && managerStream !== this.dependencies.getOriginalCameraStream()) {
|
|
2314
|
-
|
|
2796
|
+
stopStreamTracks2(managerStream);
|
|
2315
2797
|
this.dependencies.streamManager.setMediaStream(null);
|
|
2316
2798
|
}
|
|
2317
2799
|
if (isRecording) {
|
|
@@ -2639,7 +3121,7 @@ class SourceSwitchManager {
|
|
|
2639
3121
|
callbacks: this.callbacks,
|
|
2640
3122
|
streamManager: this.streamManager,
|
|
2641
3123
|
combineScreenShareWithOriginalAudio: (screenVideoTrack) => this.combineScreenShareWithOriginalAudio(screenVideoTrack),
|
|
2642
|
-
stopStreamTracks: (stream) =>
|
|
3124
|
+
stopStreamTracks: (stream) => stopStreamTracks2(stream),
|
|
2643
3125
|
stopStreamVideoTracks: (stream) => stopStreamVideoTracks(stream),
|
|
2644
3126
|
getCurrentSourceType: () => this.currentSourceType,
|
|
2645
3127
|
setCurrentSourceType: (sourceType) => {
|
|
@@ -3909,7 +4391,7 @@ class CameraStreamManager {
|
|
|
3909
4391
|
// package.json
|
|
3910
4392
|
var package_default = {
|
|
3911
4393
|
name: "@vidtreo/recorder",
|
|
3912
|
-
version: "1.
|
|
4394
|
+
version: "1.4.0",
|
|
3913
4395
|
type: "module",
|
|
3914
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.",
|
|
3915
4397
|
main: "./dist/index.js",
|
|
@@ -18258,6 +18740,7 @@ export {
|
|
|
18258
18740
|
extractVideoDuration,
|
|
18259
18741
|
extractLastFrame,
|
|
18260
18742
|
extractErrorMessage,
|
|
18743
|
+
createPermissionFlowOrchestratorDependencies,
|
|
18261
18744
|
checkRecorderSupport,
|
|
18262
18745
|
calculateVideoBitrate,
|
|
18263
18746
|
calculateTotalBitrateFromMbPerMinute,
|
|
@@ -18271,6 +18754,7 @@ export {
|
|
|
18271
18754
|
RecordingManager,
|
|
18272
18755
|
RecorderController,
|
|
18273
18756
|
QuotaManager,
|
|
18757
|
+
PermissionFlowOrchestrator,
|
|
18274
18758
|
PRESET_SIZE_LIMIT_MB_PER_MINUTE,
|
|
18275
18759
|
NativeCameraHandler,
|
|
18276
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",
|