react-media-manager 0.1.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.mjs ADDED
@@ -0,0 +1,1091 @@
1
+ import { createContext, useContext, useState, useRef, useEffect, useMemo } from 'react';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ // src/context/MediaProvider.tsx
5
+ var defaultConfig = {
6
+ autoRefreshOnDeviceChange: true,
7
+ cameraConstraints: {},
8
+ microphoneConstraints: {},
9
+ monitorMicrophoneVolume: true
10
+ };
11
+ var MediaConfigContext = createContext(defaultConfig);
12
+ function MediaProvider({ children, value }) {
13
+ return /* @__PURE__ */ jsx(MediaConfigContext.Provider, { value: { ...defaultConfig, ...value }, children });
14
+ }
15
+ function useMediaProviderConfig() {
16
+ return useContext(MediaConfigContext);
17
+ }
18
+
19
+ // src/constants/index.ts
20
+ var DEFAULT_PERMISSION_STATE = "unknown";
21
+ var DEFAULT_STATUS = "idle";
22
+
23
+ // src/utils/browser.ts
24
+ function canUseDOM() {
25
+ return typeof window !== "undefined" && typeof navigator !== "undefined";
26
+ }
27
+ function getMediaDevices() {
28
+ if (!canUseDOM() || !navigator.mediaDevices) {
29
+ return null;
30
+ }
31
+ return navigator.mediaDevices;
32
+ }
33
+ function isMediaDevicesSupported() {
34
+ const mediaDevices = getMediaDevices();
35
+ return Boolean(mediaDevices && mediaDevices.enumerateDevices && mediaDevices.getUserMedia);
36
+ }
37
+ function isPermissionsSupported() {
38
+ return canUseDOM() && typeof navigator.permissions?.query === "function";
39
+ }
40
+ function isMediaRecorderSupported() {
41
+ return canUseDOM() && typeof window.MediaRecorder !== "undefined";
42
+ }
43
+ function isSetSinkIdSupported(target) {
44
+ return Boolean(target && typeof target.setSinkId === "function");
45
+ }
46
+ function getAudioContext() {
47
+ if (!canUseDOM()) {
48
+ return void 0;
49
+ }
50
+ return window.AudioContext || window.webkitAudioContext;
51
+ }
52
+
53
+ // src/utils/errors.ts
54
+ var ERROR_MESSAGES = {
55
+ abort: "The media operation was aborted before it could complete.",
56
+ "browser-not-supported": "This browser does not support the required media APIs.",
57
+ "device-in-use": "The requested media device is already in use by another application.",
58
+ "device-not-found": "The requested media device could not be found.",
59
+ "invalid-state": "The media device is not in a valid state for this operation.",
60
+ "not-allowed": "Access to the requested media device was blocked.",
61
+ "not-readable": "The requested media device could not be read.",
62
+ overconstrained: "No device matched the requested media constraints.",
63
+ "permission-denied": "Permission to access the requested media device was denied.",
64
+ "permission-query-failed": "The browser could not report the current permission state.",
65
+ "recorder-not-supported": "This browser does not support media recording.",
66
+ "speaker-not-supported": "This browser does not support switching audio output devices.",
67
+ unknown: "An unexpected media error occurred."
68
+ };
69
+ var DOM_ERROR_CODE_MAP = {
70
+ AbortError: "abort",
71
+ InvalidStateError: "invalid-state",
72
+ NotAllowedError: "permission-denied",
73
+ NotFoundError: "device-not-found",
74
+ NotReadableError: "not-readable",
75
+ OverconstrainedError: "overconstrained",
76
+ SecurityError: "not-allowed",
77
+ TrackStartError: "device-in-use"
78
+ };
79
+ function createMediaError(code, cause, name) {
80
+ return {
81
+ code,
82
+ cause,
83
+ message: ERROR_MESSAGES[code],
84
+ name
85
+ };
86
+ }
87
+ function normalizeMediaError(error, fallbackCode = "unknown") {
88
+ if (typeof error === "object" && error && "code" in error && typeof error.code === "string" && "message" in error) {
89
+ return error;
90
+ }
91
+ if (error instanceof DOMException) {
92
+ const code = DOM_ERROR_CODE_MAP[error.name] ?? fallbackCode;
93
+ return createMediaError(code, error, error.name);
94
+ }
95
+ if (error instanceof Error) {
96
+ const code = DOM_ERROR_CODE_MAP[error.name] ?? fallbackCode;
97
+ return {
98
+ code,
99
+ cause: error,
100
+ message: error.message || ERROR_MESSAGES[code],
101
+ name: error.name
102
+ };
103
+ }
104
+ return createMediaError(fallbackCode, error);
105
+ }
106
+
107
+ // src/utils/device.ts
108
+ function mapKind(kind) {
109
+ if (kind === "videoinput") {
110
+ return "camera";
111
+ }
112
+ if (kind === "audioinput") {
113
+ return "microphone";
114
+ }
115
+ if (kind === "audiooutput") {
116
+ return "speaker";
117
+ }
118
+ return null;
119
+ }
120
+ function inferFacingMode(label) {
121
+ const normalized = label.toLowerCase();
122
+ if (normalized.includes("front") || normalized.includes("user") || normalized.includes("facetime")) {
123
+ return "user";
124
+ }
125
+ if (normalized.includes("back") || normalized.includes("rear") || normalized.includes("environment")) {
126
+ return "environment";
127
+ }
128
+ return void 0;
129
+ }
130
+ function normalizeDevice(device) {
131
+ const kind = mapKind(device.kind);
132
+ if (!kind) {
133
+ return null;
134
+ }
135
+ const base = {
136
+ deviceId: device.deviceId,
137
+ groupId: device.groupId,
138
+ kind,
139
+ label: device.label,
140
+ raw: device
141
+ };
142
+ if (kind === "camera") {
143
+ return {
144
+ ...base,
145
+ facingMode: inferFacingMode(device.label)
146
+ };
147
+ }
148
+ return base;
149
+ }
150
+ function emptyDevices() {
151
+ return {
152
+ cameras: [],
153
+ microphones: [],
154
+ speakers: []
155
+ };
156
+ }
157
+ async function enumerateDevices() {
158
+ if (!isMediaDevicesSupported()) {
159
+ throw createMediaError("browser-not-supported");
160
+ }
161
+ const rawDevices = await getMediaDevices().enumerateDevices();
162
+ const grouped = emptyDevices();
163
+ rawDevices.forEach((rawDevice) => {
164
+ const device = normalizeDevice(rawDevice);
165
+ if (!device) {
166
+ return;
167
+ }
168
+ if (device.kind === "camera") {
169
+ grouped.cameras.push(device);
170
+ return;
171
+ }
172
+ if (device.kind === "microphone") {
173
+ grouped.microphones.push(device);
174
+ return;
175
+ }
176
+ grouped.speakers.push(device);
177
+ });
178
+ return grouped;
179
+ }
180
+ function flattenDevices(collection) {
181
+ return [...collection.cameras, ...collection.microphones, ...collection.speakers];
182
+ }
183
+ function findDeviceById(devices, deviceId) {
184
+ if (!deviceId) {
185
+ return devices[0] ?? null;
186
+ }
187
+ return devices.find((device) => device.deviceId === deviceId) ?? null;
188
+ }
189
+
190
+ // src/utils/permissions.ts
191
+ async function queryPermissionState(name) {
192
+ if (!isPermissionsSupported()) {
193
+ return "unsupported";
194
+ }
195
+ try {
196
+ const status = await navigator.permissions.query({ name });
197
+ return status.state;
198
+ } catch {
199
+ return "unsupported";
200
+ }
201
+ }
202
+ async function watchPermissionState(name, onChange) {
203
+ if (!isPermissionsSupported()) {
204
+ return () => void 0;
205
+ }
206
+ try {
207
+ const status = await navigator.permissions.query({ name });
208
+ const listener = () => onChange(status.state);
209
+ status.addEventListener("change", listener);
210
+ return () => status.removeEventListener("change", listener);
211
+ } catch {
212
+ return () => void 0;
213
+ }
214
+ }
215
+ async function requestMediaPermission(constraints) {
216
+ if (!isMediaDevicesSupported()) {
217
+ throw new Error("MediaDevices API is unavailable.");
218
+ }
219
+ return navigator.mediaDevices.getUserMedia(constraints);
220
+ }
221
+
222
+ // src/utils/stream.ts
223
+ function stopStream(stream) {
224
+ stream?.getTracks().forEach((track) => track.stop());
225
+ }
226
+ function setStreamEnabled(stream, enabled) {
227
+ stream?.getTracks().forEach((track) => {
228
+ track.enabled = enabled;
229
+ });
230
+ }
231
+ function buildVideoConstraints(deviceId, constraints = {}) {
232
+ return {
233
+ ...constraints,
234
+ ...deviceId ? { deviceId: { exact: deviceId } } : {}
235
+ };
236
+ }
237
+ function buildAudioConstraints(deviceId, constraints = {}) {
238
+ return {
239
+ ...constraints,
240
+ ...deviceId ? { deviceId: { exact: deviceId } } : {}
241
+ };
242
+ }
243
+ function getTrackDeviceId(stream, kind) {
244
+ const track = kind === "audio" ? stream?.getAudioTracks()[0] : stream?.getVideoTracks()[0];
245
+ const settings = track?.getSettings();
246
+ return settings?.deviceId;
247
+ }
248
+
249
+ // src/hooks/useCamera.ts
250
+ function useCamera(options = {}) {
251
+ const config = useMediaProviderConfig();
252
+ const supported = isMediaDevicesSupported();
253
+ const [cameras, setCameras] = useState([]);
254
+ const [currentDeviceId, setCurrentDeviceId] = useState(options.deviceId ?? null);
255
+ const [stream, setStream] = useState(null);
256
+ const [loading, setLoading] = useState(false);
257
+ const [error, setError] = useState(null);
258
+ const [permission, setPermission] = useState(DEFAULT_PERMISSION_STATE);
259
+ const [status, setStatus] = useState(DEFAULT_STATUS);
260
+ const [isPaused, setIsPaused] = useState(false);
261
+ const destroyedRef = useRef(false);
262
+ const currentDeviceIdRef = useRef(options.deviceId ?? null);
263
+ const streamRef = useRef(null);
264
+ const mergedConstraints = {
265
+ ...config.cameraConstraints,
266
+ ...options.constraints
267
+ };
268
+ const currentCamera = findDeviceById(cameras, currentDeviceId);
269
+ async function refreshDevices() {
270
+ if (!supported || destroyedRef.current) {
271
+ return;
272
+ }
273
+ try {
274
+ const devices = await enumerateDevices();
275
+ if (destroyedRef.current) {
276
+ return;
277
+ }
278
+ setCameras(devices.cameras);
279
+ const matchingDevice = findDeviceById(devices.cameras, currentDeviceIdRef.current);
280
+ if (!matchingDevice && currentDeviceIdRef.current) {
281
+ stop();
282
+ setCurrentDeviceId(devices.cameras[0]?.deviceId ?? null);
283
+ currentDeviceIdRef.current = devices.cameras[0]?.deviceId ?? null;
284
+ setError(normalizeMediaError(new DOMException("Camera not found.", "NotFoundError"), "device-not-found"));
285
+ return;
286
+ }
287
+ if (!currentDeviceIdRef.current && devices.cameras[0]) {
288
+ setCurrentDeviceId(devices.cameras[0].deviceId);
289
+ currentDeviceIdRef.current = devices.cameras[0].deviceId;
290
+ }
291
+ } catch (nextError) {
292
+ setError(normalizeMediaError(nextError, "browser-not-supported"));
293
+ setCameras(emptyDevices().cameras);
294
+ }
295
+ }
296
+ async function updatePermission() {
297
+ if (!supported || destroyedRef.current) {
298
+ return;
299
+ }
300
+ const nextPermission = await queryPermissionState("camera");
301
+ if (!destroyedRef.current) {
302
+ setPermission(nextPermission);
303
+ }
304
+ }
305
+ function stop() {
306
+ stopStream(streamRef.current);
307
+ streamRef.current = null;
308
+ setStream(null);
309
+ setIsPaused(false);
310
+ setStatus("stopped");
311
+ }
312
+ async function startStream(deviceIdOverride, override = {}) {
313
+ if (!supported) {
314
+ const nextError = normalizeMediaError(new Error("MediaDevices API is unavailable."), "browser-not-supported");
315
+ setError(nextError);
316
+ setStatus("error");
317
+ return null;
318
+ }
319
+ setLoading(true);
320
+ setError(null);
321
+ setStatus("loading");
322
+ try {
323
+ const deviceId = deviceIdOverride ?? currentDeviceIdRef.current ?? options.deviceId;
324
+ const constraints = buildVideoConstraints(deviceId ?? void 0, {
325
+ ...mergedConstraints,
326
+ ...override,
327
+ ...options.facingMode ? { facingMode: options.facingMode } : {}
328
+ });
329
+ const nextStream = await navigator.mediaDevices.getUserMedia({ video: constraints, audio: false });
330
+ const detectedDeviceId = getTrackDeviceId(nextStream, "video") ?? deviceId ?? null;
331
+ stopStream(streamRef.current);
332
+ streamRef.current = nextStream;
333
+ setStream(nextStream);
334
+ setCurrentDeviceId(detectedDeviceId);
335
+ currentDeviceIdRef.current = detectedDeviceId;
336
+ setPermission("granted");
337
+ setIsPaused(false);
338
+ setStatus("active");
339
+ return nextStream;
340
+ } catch (nextError) {
341
+ const normalized = normalizeMediaError(nextError, "unknown");
342
+ setError(normalized);
343
+ setPermission(normalized.code === "permission-denied" ? "denied" : permission);
344
+ setStatus("error");
345
+ return null;
346
+ } finally {
347
+ setLoading(false);
348
+ }
349
+ }
350
+ async function start(override = {}) {
351
+ return startStream(void 0, override);
352
+ }
353
+ function pause() {
354
+ setStreamEnabled(streamRef.current, false);
355
+ setIsPaused(true);
356
+ setStatus("paused");
357
+ }
358
+ function resume() {
359
+ setStreamEnabled(streamRef.current, true);
360
+ setIsPaused(false);
361
+ setStatus(streamRef.current ? "active" : "idle");
362
+ }
363
+ async function switchCamera(deviceId, override = {}) {
364
+ setCurrentDeviceId(deviceId);
365
+ currentDeviceIdRef.current = deviceId;
366
+ if (!streamRef.current) {
367
+ return null;
368
+ }
369
+ return startStream(deviceId, override);
370
+ }
371
+ async function refresh() {
372
+ await Promise.all([refreshDevices(), updatePermission()]);
373
+ }
374
+ function destroy() {
375
+ destroyedRef.current = true;
376
+ stop();
377
+ setCameras([]);
378
+ setError(null);
379
+ setPermission(DEFAULT_PERMISSION_STATE);
380
+ setStatus(DEFAULT_STATUS);
381
+ currentDeviceIdRef.current = null;
382
+ }
383
+ useEffect(() => {
384
+ destroyedRef.current = false;
385
+ void refresh();
386
+ if (config.autoRefreshOnDeviceChange && supported) {
387
+ const handleDeviceChange = () => {
388
+ void refreshDevices();
389
+ };
390
+ navigator.mediaDevices.addEventListener("devicechange", handleDeviceChange);
391
+ return () => {
392
+ destroyedRef.current = true;
393
+ stopStream(streamRef.current);
394
+ navigator.mediaDevices.removeEventListener("devicechange", handleDeviceChange);
395
+ };
396
+ }
397
+ return () => {
398
+ destroyedRef.current = true;
399
+ stopStream(streamRef.current);
400
+ };
401
+ }, [config.autoRefreshOnDeviceChange, supported]);
402
+ useEffect(() => {
403
+ if (options.autoStart) {
404
+ void start();
405
+ }
406
+ }, []);
407
+ return {
408
+ cameras,
409
+ constraints: mergedConstraints,
410
+ currentCamera,
411
+ destroy,
412
+ error,
413
+ isActive: Boolean(stream),
414
+ isLoading: loading,
415
+ isPaused,
416
+ loading,
417
+ pause,
418
+ permission,
419
+ refresh,
420
+ refreshDevices,
421
+ resume,
422
+ start,
423
+ status,
424
+ stop,
425
+ stream,
426
+ supported,
427
+ switchCamera
428
+ };
429
+ }
430
+ function useMediaDevices(options = {}) {
431
+ const config = useMediaProviderConfig();
432
+ const shouldListen = options.listen ?? config.autoRefreshOnDeviceChange;
433
+ const supported = isMediaDevicesSupported();
434
+ const [devices, setDevices] = useState(emptyDevices);
435
+ const [loading, setLoading] = useState(false);
436
+ const [error, setError] = useState(null);
437
+ const destroyedRef = useRef(false);
438
+ async function refresh() {
439
+ if (!supported || destroyedRef.current) {
440
+ return;
441
+ }
442
+ setLoading(true);
443
+ setError(null);
444
+ try {
445
+ const nextDevices = await enumerateDevices();
446
+ if (!destroyedRef.current) {
447
+ setDevices(nextDevices);
448
+ }
449
+ } catch (nextError) {
450
+ if (!destroyedRef.current) {
451
+ setError(normalizeMediaError(nextError, "browser-not-supported"));
452
+ setDevices(emptyDevices());
453
+ }
454
+ } finally {
455
+ if (!destroyedRef.current) {
456
+ setLoading(false);
457
+ }
458
+ }
459
+ }
460
+ function destroy() {
461
+ destroyedRef.current = true;
462
+ setDevices(emptyDevices());
463
+ setError(null);
464
+ setLoading(false);
465
+ }
466
+ useEffect(() => {
467
+ destroyedRef.current = false;
468
+ void refresh();
469
+ if (!supported || !shouldListen) {
470
+ return () => {
471
+ destroyedRef.current = true;
472
+ };
473
+ }
474
+ const mediaDevices = navigator.mediaDevices;
475
+ const handleDeviceChange = () => {
476
+ void refresh();
477
+ };
478
+ mediaDevices.addEventListener("devicechange", handleDeviceChange);
479
+ return () => {
480
+ destroyedRef.current = true;
481
+ mediaDevices.removeEventListener("devicechange", handleDeviceChange);
482
+ };
483
+ }, [shouldListen, supported]);
484
+ return {
485
+ ...devices,
486
+ destroy,
487
+ devices: flattenDevices(devices),
488
+ error,
489
+ loading,
490
+ refresh,
491
+ supported
492
+ };
493
+ }
494
+
495
+ // src/hooks/useDeviceChange.ts
496
+ function useDeviceChange() {
497
+ const deviceState = useMediaDevices({ listen: false });
498
+ const supported = isMediaDevicesSupported();
499
+ const [changeCount, setChangeCount] = useState(0);
500
+ const [lastChangeAt, setLastChangeAt] = useState(null);
501
+ useEffect(() => {
502
+ if (!supported) {
503
+ return void 0;
504
+ }
505
+ const handleDeviceChange = () => {
506
+ setChangeCount((current) => current + 1);
507
+ setLastChangeAt(Date.now());
508
+ void deviceState.refresh();
509
+ };
510
+ navigator.mediaDevices.addEventListener("devicechange", handleDeviceChange);
511
+ return () => {
512
+ navigator.mediaDevices.removeEventListener("devicechange", handleDeviceChange);
513
+ };
514
+ }, [deviceState, supported]);
515
+ return {
516
+ ...deviceState,
517
+ changeCount,
518
+ lastChangeAt,
519
+ supported
520
+ };
521
+ }
522
+ function useMediaRecorder() {
523
+ const supported = isMediaRecorderSupported();
524
+ const [status, setStatus] = useState("idle");
525
+ const [loading, setLoading] = useState(false);
526
+ const [error, setError] = useState(null);
527
+ const [chunks, setChunks] = useState([]);
528
+ const [blob, setBlob] = useState(null);
529
+ const recorderRef = useRef(null);
530
+ const destroyedRef = useRef(false);
531
+ const url = useMemo(() => {
532
+ if (!blob || typeof URL === "undefined") {
533
+ return null;
534
+ }
535
+ return URL.createObjectURL(blob);
536
+ }, [blob]);
537
+ function reset() {
538
+ setChunks([]);
539
+ setBlob(null);
540
+ setError(null);
541
+ setStatus("idle");
542
+ }
543
+ function destroy() {
544
+ destroyedRef.current = true;
545
+ recorderRef.current?.stop();
546
+ recorderRef.current = null;
547
+ reset();
548
+ }
549
+ function refresh() {
550
+ return Promise.resolve();
551
+ }
552
+ function start(stream, options = {}) {
553
+ if (!supported || typeof window.MediaRecorder === "undefined") {
554
+ setError(normalizeMediaError(new Error("MediaRecorder is unavailable."), "recorder-not-supported"));
555
+ setStatus("error");
556
+ return;
557
+ }
558
+ setLoading(true);
559
+ setError(null);
560
+ setChunks([]);
561
+ setBlob(null);
562
+ try {
563
+ const recorder = new window.MediaRecorder(stream, options);
564
+ recorder.ondataavailable = (event) => {
565
+ if (event.data && event.data.size > 0) {
566
+ setChunks((current) => [...current, event.data]);
567
+ }
568
+ };
569
+ recorder.onstop = () => {
570
+ setStatus("stopped");
571
+ setLoading(false);
572
+ };
573
+ recorder.onerror = (event) => {
574
+ setError(normalizeMediaError(event.error, "unknown"));
575
+ setStatus("error");
576
+ setLoading(false);
577
+ };
578
+ recorderRef.current = recorder;
579
+ recorder.start();
580
+ setStatus("active");
581
+ } catch (nextError) {
582
+ setError(normalizeMediaError(nextError, "unknown"));
583
+ setStatus("error");
584
+ setLoading(false);
585
+ }
586
+ }
587
+ function stop() {
588
+ recorderRef.current?.stop();
589
+ }
590
+ function pause() {
591
+ recorderRef.current?.pause();
592
+ setStatus("paused");
593
+ }
594
+ function resume() {
595
+ recorderRef.current?.resume();
596
+ setStatus("active");
597
+ }
598
+ function download(fileName = "recording.webm") {
599
+ const nextBlob = blob ?? (chunks.length ? new Blob(chunks, { type: chunks[0]?.type || "video/webm" }) : null);
600
+ if (!nextBlob || typeof document === "undefined" || typeof URL === "undefined") {
601
+ return null;
602
+ }
603
+ const nextUrl = URL.createObjectURL(nextBlob);
604
+ const anchor = document.createElement("a");
605
+ anchor.href = nextUrl;
606
+ anchor.download = fileName;
607
+ anchor.click();
608
+ return nextUrl;
609
+ }
610
+ useEffect(() => {
611
+ if (chunks.length) {
612
+ setBlob(new Blob(chunks, { type: chunks[0]?.type || "video/webm" }));
613
+ }
614
+ }, [chunks]);
615
+ useEffect(() => {
616
+ return () => {
617
+ if (url) {
618
+ URL.revokeObjectURL(url);
619
+ }
620
+ };
621
+ }, [url]);
622
+ useEffect(() => {
623
+ return () => {
624
+ destroyedRef.current = true;
625
+ if (url) {
626
+ URL.revokeObjectURL(url);
627
+ }
628
+ recorderRef.current = null;
629
+ };
630
+ }, [url]);
631
+ return {
632
+ blob,
633
+ chunks,
634
+ destroy,
635
+ download,
636
+ error,
637
+ isPaused: status === "paused",
638
+ isRecording: status === "active",
639
+ loading,
640
+ pause,
641
+ refresh,
642
+ reset,
643
+ resume,
644
+ start,
645
+ status,
646
+ stop,
647
+ supported,
648
+ url
649
+ };
650
+ }
651
+ function useMicrophone(options = {}) {
652
+ const config = useMediaProviderConfig();
653
+ const supported = isMediaDevicesSupported();
654
+ const [microphones, setMicrophones] = useState([]);
655
+ const [currentDeviceId, setCurrentDeviceId] = useState(options.deviceId ?? null);
656
+ const [stream, setStream] = useState(null);
657
+ const [loading, setLoading] = useState(false);
658
+ const [error, setError] = useState(null);
659
+ const [permission, setPermission] = useState(DEFAULT_PERMISSION_STATE);
660
+ const [status, setStatus] = useState(DEFAULT_STATUS);
661
+ const [muted, setMuted] = useState(false);
662
+ const [volume, setVolume] = useState(0);
663
+ const destroyedRef = useRef(false);
664
+ const currentDeviceIdRef = useRef(options.deviceId ?? null);
665
+ const streamRef = useRef(null);
666
+ const analyserContextRef = useRef(null);
667
+ const animationFrameRef = useRef(null);
668
+ const mergedConstraints = {
669
+ ...config.microphoneConstraints,
670
+ ...options.constraints
671
+ };
672
+ const currentMicrophone = findDeviceById(microphones, currentDeviceId);
673
+ function stopVolumeMonitor() {
674
+ if (animationFrameRef.current) {
675
+ cancelAnimationFrame(animationFrameRef.current);
676
+ animationFrameRef.current = null;
677
+ }
678
+ if (analyserContextRef.current) {
679
+ void analyserContextRef.current.close();
680
+ analyserContextRef.current = null;
681
+ }
682
+ setVolume(0);
683
+ }
684
+ function startVolumeMonitor(nextStream) {
685
+ if (!(options.monitorVolume ?? config.monitorMicrophoneVolume)) {
686
+ return;
687
+ }
688
+ const AudioContextCtor = getAudioContext();
689
+ if (!AudioContextCtor) {
690
+ return;
691
+ }
692
+ const audioContext = new AudioContextCtor();
693
+ const analyser = audioContext.createAnalyser();
694
+ const source = audioContext.createMediaStreamSource(nextStream);
695
+ const sample = new Uint8Array(analyser.fftSize);
696
+ analyser.smoothingTimeConstant = 0.8;
697
+ source.connect(analyser);
698
+ analyserContextRef.current = audioContext;
699
+ const readVolume = () => {
700
+ analyser.getByteTimeDomainData(sample);
701
+ let sum = 0;
702
+ sample.forEach((value) => {
703
+ const normalized = (value - 128) / 128;
704
+ sum += normalized * normalized;
705
+ });
706
+ setVolume(Number(Math.min(1, Math.sqrt(sum / sample.length)).toFixed(4)));
707
+ animationFrameRef.current = requestAnimationFrame(readVolume);
708
+ };
709
+ animationFrameRef.current = requestAnimationFrame(readVolume);
710
+ }
711
+ async function refreshDevices() {
712
+ if (!supported || destroyedRef.current) {
713
+ return;
714
+ }
715
+ try {
716
+ const devices = await enumerateDevices();
717
+ if (destroyedRef.current) {
718
+ return;
719
+ }
720
+ setMicrophones(devices.microphones);
721
+ const matchingDevice = findDeviceById(devices.microphones, currentDeviceIdRef.current);
722
+ if (!matchingDevice && currentDeviceIdRef.current) {
723
+ stop();
724
+ setCurrentDeviceId(devices.microphones[0]?.deviceId ?? null);
725
+ currentDeviceIdRef.current = devices.microphones[0]?.deviceId ?? null;
726
+ setError(normalizeMediaError(new DOMException("Microphone not found.", "NotFoundError"), "device-not-found"));
727
+ return;
728
+ }
729
+ if (!currentDeviceIdRef.current && devices.microphones[0]) {
730
+ setCurrentDeviceId(devices.microphones[0].deviceId);
731
+ currentDeviceIdRef.current = devices.microphones[0].deviceId;
732
+ }
733
+ } catch (nextError) {
734
+ setError(normalizeMediaError(nextError, "browser-not-supported"));
735
+ setMicrophones(emptyDevices().microphones);
736
+ }
737
+ }
738
+ async function updatePermission() {
739
+ if (!supported || destroyedRef.current) {
740
+ return;
741
+ }
742
+ const nextPermission = await queryPermissionState("microphone");
743
+ if (!destroyedRef.current) {
744
+ setPermission(nextPermission);
745
+ }
746
+ }
747
+ function stop() {
748
+ stopVolumeMonitor();
749
+ stopStream(streamRef.current);
750
+ streamRef.current = null;
751
+ setStream(null);
752
+ setMuted(false);
753
+ setStatus("stopped");
754
+ }
755
+ async function startStream(deviceIdOverride, override = {}) {
756
+ if (!supported) {
757
+ const nextError = normalizeMediaError(new Error("MediaDevices API is unavailable."), "browser-not-supported");
758
+ setError(nextError);
759
+ setStatus("error");
760
+ return null;
761
+ }
762
+ setLoading(true);
763
+ setError(null);
764
+ setStatus("loading");
765
+ try {
766
+ const deviceId = deviceIdOverride ?? currentDeviceIdRef.current ?? options.deviceId;
767
+ const constraints = buildAudioConstraints(deviceId ?? void 0, {
768
+ ...mergedConstraints,
769
+ ...override
770
+ });
771
+ const nextStream = await navigator.mediaDevices.getUserMedia({ video: false, audio: constraints });
772
+ const detectedDeviceId = getTrackDeviceId(nextStream, "audio") ?? deviceId ?? null;
773
+ stop();
774
+ streamRef.current = nextStream;
775
+ setStream(nextStream);
776
+ setCurrentDeviceId(detectedDeviceId);
777
+ currentDeviceIdRef.current = detectedDeviceId;
778
+ setPermission("granted");
779
+ setMuted(false);
780
+ setStatus("active");
781
+ startVolumeMonitor(nextStream);
782
+ return nextStream;
783
+ } catch (nextError) {
784
+ const normalized = normalizeMediaError(nextError, "unknown");
785
+ setError(normalized);
786
+ setPermission(normalized.code === "permission-denied" ? "denied" : permission);
787
+ setStatus("error");
788
+ return null;
789
+ } finally {
790
+ setLoading(false);
791
+ }
792
+ }
793
+ async function start(override = {}) {
794
+ return startStream(void 0, override);
795
+ }
796
+ function mute() {
797
+ setStreamEnabled(streamRef.current, false);
798
+ setMuted(true);
799
+ }
800
+ function unmute() {
801
+ setStreamEnabled(streamRef.current, true);
802
+ setMuted(false);
803
+ }
804
+ async function switchMicrophone(deviceId, override = {}) {
805
+ setCurrentDeviceId(deviceId);
806
+ currentDeviceIdRef.current = deviceId;
807
+ if (!streamRef.current) {
808
+ return null;
809
+ }
810
+ return startStream(deviceId, override);
811
+ }
812
+ async function refresh() {
813
+ await Promise.all([refreshDevices(), updatePermission()]);
814
+ }
815
+ function destroy() {
816
+ destroyedRef.current = true;
817
+ stop();
818
+ setMicrophones([]);
819
+ setError(null);
820
+ setPermission(DEFAULT_PERMISSION_STATE);
821
+ setStatus(DEFAULT_STATUS);
822
+ currentDeviceIdRef.current = null;
823
+ }
824
+ useEffect(() => {
825
+ destroyedRef.current = false;
826
+ void refresh();
827
+ if (config.autoRefreshOnDeviceChange && supported) {
828
+ const handleDeviceChange = () => {
829
+ void refreshDevices();
830
+ };
831
+ navigator.mediaDevices.addEventListener("devicechange", handleDeviceChange);
832
+ return () => {
833
+ destroyedRef.current = true;
834
+ stopVolumeMonitor();
835
+ stopStream(streamRef.current);
836
+ navigator.mediaDevices.removeEventListener("devicechange", handleDeviceChange);
837
+ };
838
+ }
839
+ return () => {
840
+ destroyedRef.current = true;
841
+ stopVolumeMonitor();
842
+ stopStream(streamRef.current);
843
+ };
844
+ }, [config.autoRefreshOnDeviceChange, supported]);
845
+ useEffect(() => {
846
+ if (options.autoStart) {
847
+ void start();
848
+ }
849
+ }, []);
850
+ return {
851
+ currentMicrophone,
852
+ destroy,
853
+ error,
854
+ isActive: Boolean(stream),
855
+ isLoading: loading,
856
+ loading,
857
+ microphones,
858
+ mute,
859
+ muted,
860
+ permission,
861
+ refresh,
862
+ refreshDevices,
863
+ start,
864
+ status,
865
+ stop,
866
+ stream,
867
+ supported,
868
+ switchMicrophone,
869
+ unmute,
870
+ volume
871
+ };
872
+ }
873
+ function usePermissions() {
874
+ const supported = isMediaDevicesSupported();
875
+ const permissionsSupported = isPermissionsSupported();
876
+ const [camera, setCamera] = useState(DEFAULT_PERMISSION_STATE);
877
+ const [microphone, setMicrophone] = useState(DEFAULT_PERMISSION_STATE);
878
+ const [loading, setLoading] = useState(false);
879
+ const [error, setError] = useState(null);
880
+ const destroyedRef = useRef(false);
881
+ async function refresh() {
882
+ if (!supported || destroyedRef.current) {
883
+ return;
884
+ }
885
+ setLoading(true);
886
+ setError(null);
887
+ try {
888
+ const [nextCamera, nextMicrophone] = await Promise.all([
889
+ queryPermissionState("camera"),
890
+ queryPermissionState("microphone")
891
+ ]);
892
+ if (!destroyedRef.current) {
893
+ setCamera(nextCamera);
894
+ setMicrophone(nextMicrophone);
895
+ }
896
+ } catch (nextError) {
897
+ if (!destroyedRef.current) {
898
+ setError(normalizeMediaError(nextError, "permission-query-failed"));
899
+ }
900
+ } finally {
901
+ if (!destroyedRef.current) {
902
+ setLoading(false);
903
+ }
904
+ }
905
+ }
906
+ async function requestCamera() {
907
+ try {
908
+ const stream = await requestMediaPermission({ video: true });
909
+ stopStream(stream);
910
+ await refresh();
911
+ } catch (nextError) {
912
+ setError(normalizeMediaError(nextError, "permission-denied"));
913
+ throw nextError;
914
+ }
915
+ }
916
+ async function requestMicrophone() {
917
+ try {
918
+ const stream = await requestMediaPermission({ audio: true });
919
+ stopStream(stream);
920
+ await refresh();
921
+ } catch (nextError) {
922
+ setError(normalizeMediaError(nextError, "permission-denied"));
923
+ throw nextError;
924
+ }
925
+ }
926
+ async function requestAll() {
927
+ try {
928
+ const stream = await requestMediaPermission({ audio: true, video: true });
929
+ stopStream(stream);
930
+ await refresh();
931
+ } catch (nextError) {
932
+ setError(normalizeMediaError(nextError, "permission-denied"));
933
+ throw nextError;
934
+ }
935
+ }
936
+ function destroy() {
937
+ destroyedRef.current = true;
938
+ setCamera(DEFAULT_PERMISSION_STATE);
939
+ setMicrophone(DEFAULT_PERMISSION_STATE);
940
+ setError(null);
941
+ setLoading(false);
942
+ }
943
+ useEffect(() => {
944
+ destroyedRef.current = false;
945
+ void refresh();
946
+ if (!permissionsSupported) {
947
+ setCamera("unsupported");
948
+ setMicrophone("unsupported");
949
+ return () => {
950
+ destroyedRef.current = true;
951
+ };
952
+ }
953
+ let cameraCleanup = () => void 0;
954
+ let microphoneCleanup = () => void 0;
955
+ void watchPermissionState("camera", (nextState) => {
956
+ if (!destroyedRef.current) {
957
+ setCamera(nextState);
958
+ }
959
+ }).then((cleanup) => {
960
+ cameraCleanup = cleanup;
961
+ });
962
+ void watchPermissionState("microphone", (nextState) => {
963
+ if (!destroyedRef.current) {
964
+ setMicrophone(nextState);
965
+ }
966
+ }).then((cleanup) => {
967
+ microphoneCleanup = cleanup;
968
+ });
969
+ return () => {
970
+ destroyedRef.current = true;
971
+ cameraCleanup();
972
+ microphoneCleanup();
973
+ };
974
+ }, [permissionsSupported, supported]);
975
+ return {
976
+ camera,
977
+ destroy,
978
+ error,
979
+ loading,
980
+ microphone,
981
+ refresh,
982
+ requestAll,
983
+ requestCamera,
984
+ requestMicrophone,
985
+ supported
986
+ };
987
+ }
988
+ function resolveTarget(target) {
989
+ if (!target) {
990
+ return null;
991
+ }
992
+ if ("current" in target) {
993
+ return target.current;
994
+ }
995
+ return target;
996
+ }
997
+ function useSpeaker(target) {
998
+ const supported = isMediaDevicesSupported();
999
+ const [speakers, setSpeakers] = useState([]);
1000
+ const [currentSpeakerId, setCurrentSpeakerId] = useState(null);
1001
+ const [loading, setLoading] = useState(false);
1002
+ const [error, setError] = useState(null);
1003
+ const destroyedRef = useRef(false);
1004
+ const permission = "unknown";
1005
+ const currentSpeaker = findDeviceById(speakers, currentSpeakerId);
1006
+ const mediaElement = resolveTarget(target);
1007
+ const sinkSupported = isSetSinkIdSupported(mediaElement);
1008
+ async function refresh() {
1009
+ if (!supported || destroyedRef.current) {
1010
+ return;
1011
+ }
1012
+ setLoading(true);
1013
+ setError(null);
1014
+ try {
1015
+ const devices = await enumerateDevices();
1016
+ if (!destroyedRef.current) {
1017
+ setSpeakers(devices.speakers);
1018
+ if (!currentSpeakerId && devices.speakers[0]) {
1019
+ setCurrentSpeakerId(devices.speakers[0].deviceId);
1020
+ }
1021
+ }
1022
+ } catch (nextError) {
1023
+ if (!destroyedRef.current) {
1024
+ setError(normalizeMediaError(nextError, "browser-not-supported"));
1025
+ setSpeakers(emptyDevices().speakers);
1026
+ }
1027
+ } finally {
1028
+ if (!destroyedRef.current) {
1029
+ setLoading(false);
1030
+ }
1031
+ }
1032
+ }
1033
+ async function switchSpeaker(deviceId) {
1034
+ if (!mediaElement || !sinkSupported) {
1035
+ setError(normalizeMediaError(new Error("setSinkId is unavailable."), "speaker-not-supported"));
1036
+ return false;
1037
+ }
1038
+ try {
1039
+ await mediaElement.setSinkId(deviceId);
1040
+ setCurrentSpeakerId(deviceId);
1041
+ setError(null);
1042
+ return true;
1043
+ } catch (nextError) {
1044
+ setError(normalizeMediaError(nextError, "speaker-not-supported"));
1045
+ return false;
1046
+ }
1047
+ }
1048
+ function destroy() {
1049
+ destroyedRef.current = true;
1050
+ setSpeakers([]);
1051
+ setCurrentSpeakerId(null);
1052
+ setError(null);
1053
+ setLoading(false);
1054
+ }
1055
+ useEffect(() => {
1056
+ destroyedRef.current = false;
1057
+ void refresh();
1058
+ if (!supported) {
1059
+ return () => {
1060
+ destroyedRef.current = true;
1061
+ };
1062
+ }
1063
+ const handleDeviceChange = () => {
1064
+ void refresh();
1065
+ };
1066
+ navigator.mediaDevices.addEventListener("devicechange", handleDeviceChange);
1067
+ return () => {
1068
+ destroyedRef.current = true;
1069
+ navigator.mediaDevices.removeEventListener("devicechange", handleDeviceChange);
1070
+ };
1071
+ }, [supported]);
1072
+ return {
1073
+ currentSpeaker,
1074
+ destroy,
1075
+ error,
1076
+ isSupported: sinkSupported,
1077
+ loading,
1078
+ permission,
1079
+ refresh,
1080
+ speakers,
1081
+ supported,
1082
+ switchSpeaker
1083
+ };
1084
+ }
1085
+
1086
+ // src/version.ts
1087
+ var version = "0.1.0";
1088
+
1089
+ export { MediaProvider, useCamera, useDeviceChange, useMediaDevices, useMediaProviderConfig, useMediaRecorder, useMicrophone, usePermissions, useSpeaker, version };
1090
+ //# sourceMappingURL=out.js.map
1091
+ //# sourceMappingURL=index.mjs.map