forked from StellarSplit/StellarSplit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseCameraStream.ts
More file actions
118 lines (103 loc) · 3.61 KB
/
Copy pathuseCameraStream.ts
File metadata and controls
118 lines (103 loc) · 3.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/**
* useCameraStream.ts — Issue #489
*
* Shared camera-stream lifecycle hook for QRCodeScanner and CameraCapture.
* Owns permission checks, stream start/stop, error mapping, and cleanup on unmount
* so neither component manages raw MediaStream state independently.
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import {
requestCameraPermission,
stopCameraStream,
getUserFriendlyErrorMessage,
} from '../utils/cameraPermissions';
// ── Types ──────────────────────────────────────────────────────────────────────
export type CameraStreamStatus =
| 'idle'
| 'requesting'
| 'active'
| 'error'
| 'stopped';
export interface CameraStreamError {
type: 'permission-denied' | 'not-found' | 'not-secure' | 'unknown';
message: string;
}
export interface UseCameraStreamOptions {
/** MediaStream constraints forwarded to getUserMedia. */
constraints?: MediaStreamConstraints;
/** Start the stream automatically when the hook mounts. */
autoStart?: boolean;
}
export interface UseCameraStreamReturn {
status: CameraStreamStatus;
stream: MediaStream | null;
error: CameraStreamError | null;
/** Request camera access and start the stream. */
startStream: () => Promise<void>;
/** Stop the stream and release the camera hardware. */
stopStream: () => void;
/** Stop then restart the stream (e.g. to switch cameras). */
restartStream: () => Promise<void>;
}
// ── Hook ──────────────────────────────────────────────────────────────────────
export function useCameraStream({
constraints = { video: { facingMode: 'environment' }, audio: false },
autoStart = false,
}: UseCameraStreamOptions = {}): UseCameraStreamReturn {
const [status, setStatus] = useState<CameraStreamStatus>('idle');
const [error, setError] = useState<CameraStreamError | null>(null);
const streamRef = useRef<MediaStream | null>(null);
// Track mounted state to skip setState after unmount
const mountedRef = useRef(true);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
// Always release hardware on unmount
if (streamRef.current) {
stopCameraStream(streamRef.current);
streamRef.current = null;
}
};
}, []);
const stopStream = useCallback(() => {
if (streamRef.current) {
stopCameraStream(streamRef.current);
streamRef.current = null;
}
if (mountedRef.current) setStatus('stopped');
}, []);
const startStream = useCallback(async () => {
if (!mountedRef.current) return;
setStatus('requesting');
setError(null);
try {
const stream = await requestCameraPermission(constraints);
streamRef.current = stream;
if (mountedRef.current) setStatus('active');
} catch (err) {
const message = getUserFriendlyErrorMessage(err as Error);
if (mountedRef.current) {
setStatus('error');
setError({ type: 'unknown', message });
}
}
}, [constraints]);
const restartStream = useCallback(async () => {
stopStream();
await startStream();
}, [stopStream, startStream]);
// Auto-start
useEffect(() => {
if (autoStart) void startStream();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoStart]);
return {
status,
stream: streamRef.current,
error,
startStream,
stopStream,
restartStream,
};
}