테스트한 코드를 공유드려요 
필요치 않은 부분은 무시부탁드려용
import { useState, useCallback, useRef, useEffect } from 'react'
import './App.css'
function App() {
const [isVideoStreaming, setIsVideoStreaming] = useState(false);
const [facingMode, setFacingMode] = useState<'user' | 'environment'>('environment');
const [cameraError, setCameraError] = useState<string>('');
const videoRef = useRef<HTMLVideoElement>(null);
const streamRef = useRef<MediaStream | null>(null);
useEffect(() => {
if (isVideoStreaming && streamRef.current && videoRef.current) {
const video = videoRef.current;
const stream = streamRef.current;
video.srcObject = stream;
// 비디오 이벤트 리스너
video.onloadedmetadata = () => {
console.log('useEffect: 메타데이터 로드됨');
video.play().catch(e => console.error('useEffect: play 실패', e));
};
video.onpause = () => {
console.warn('비디오가 일시정지됨! 다시 재생 시도...');
video.play().catch(e => console.error('재생 재시도 실패:', e));
};
video.onstalled = () => {
console.warn('비디오 스톨됨');
};
video.onsuspend = () => {
console.warn('비디오 서스펜드됨');
};
// 주기적으로 재생 상태 확인
const checkInterval = setInterval(() => {
if (video.paused && streamRef.current) {
console.log('비디오가 멈춤 감지, 재생 재시도');
video.play().catch(e => console.error('재생 실패:', e));
}
}, 1000);
return () => {
clearInterval(checkInterval);
};
}
}, [isVideoStreaming]);
const startVideoStreaming = useCallback(async () => {
setCameraError('');
try {
const constraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: facingMode
},
audio: false
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
// 스트림 저장 (먼저 상태 업데이트)
streamRef.current = stream;
setIsVideoStreaming(true);
setCameraError('');
// 비디오 엘리먼트가 렌더링될 때까지 약간 대기
await new Promise(resolve => setTimeout(resolve, 100));
// 비디오 태그에 스트림 연결
if (videoRef.current) {
console.log('비디오 엘리먼트에 스트림 연결 중...');
console.log('videoRef.current:', videoRef.current);
videoRef.current.onloadedmetadata = () => {
console.log('비디오 메타데이터 로드 완료');
console.log('비디오 크기:', videoRef.current?.videoWidth, 'x', videoRef.current?.videoHeight);
};
videoRef.current.onplay = () => {
console.log('비디오 재생 시작');
};
videoRef.current.onerror = (e) => {
console.error('비디오 재생 에러:', e);
};
// srcObject 설정
videoRef.current.srcObject = stream;
console.log('srcObject 설정 완료:', !!videoRef.current.srcObject);
// play 시도
try {
await videoRef.current.play();
} catch (playError) {
// 한 번 더 시도
setTimeout(async () => {
try {
await videoRef.current?.play();
console.log('재시도로 비디오 재생 성공');
} catch (e) {
console.error('재시도도 실패:', e);
}
}, 500);
}
} else {
console.error('videoRef.current가 여전히 null입니다');
}
alert('비디오 스트리밍 시작 성공!');
} catch (error: any) {
console.error('비디오 스트림 연결 실패:', error);
console.error('에러 타입:', error.constructor.name);
console.error('에러 이름:', error.name);
console.error('에러 메시지:', error.message);
console.error('에러 스택:', error.stack);
setCameraError(`비디오 스트림 실패: ${error.name} - ${error.message}`);
alert(`비디오 스트리밍 실패: ${error.name} - ${error.message}`);
}
}, [facingMode]);
const stopVideoStreaming = useCallback(() => {
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => {
track.stop();
console.log('비디오 트랙 정지:', track.label);
});
streamRef.current = null;
}
if (videoRef.current) {
videoRef.current.srcObject = null;
}
setIsVideoStreaming(false);
console.log('비디오 스트림 종료');
}, []);
const switchCamera = useCallback(async () => {
console.log('카메라 전환 시작...');
// 현재 스트림 종료
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
streamRef.current = null;
}
// 카메라 모드 전환
const newFacingMode = facingMode === 'user' ? 'environment' : 'user';
setFacingMode(newFacingMode);
console.log('카메라 모드:', newFacingMode === 'user' ? '전면' : '후면');
// 약간 대기 후 새 스트림 시작
await new Promise(resolve => setTimeout(resolve, 100));
try {
const constraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: newFacingMode
},
audio: false
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
streamRef.current = stream;
if (videoRef.current) {
videoRef.current.srcObject = stream;
await videoRef.current.play();
}
} catch (error: any) {
setCameraError(`카메라 전환 실패: ${error.message}`);
}
}, [facingMode]);
return (
<>
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
{/* 비디오 스트리밍 상태 */}
<div style={{ marginBottom: '20px', padding: '15px', backgroundColor: '#f0f0f0', borderRadius: '8px' }}>
<h3 style={{ margin: '0 0 10px 0' }}>비디오 스트리밍 상태</h3>
<p style={{ margin: '5px 0', fontSize: '16px', fontWeight: 'bold' }}>
{isVideoStreaming ? '스트리밍 중' : '스트리밍 안함'}
</p>
{isVideoStreaming && (
<p style={{ margin: '5px 0', fontSize: '14px', color: '#666' }}>
현재 카메라: {facingMode === 'user' ? '전면' : ' 후면'}
</p>
)}
</div>
{/* 실시간 비디오 스트림 */}
<div style={{
width: '100%',
maxWidth: '640px',
minHeight: '360px',
border: '3px solid ' + (isVideoStreaming ? '#00ff00' : '#ff0000'),
borderRadius: '8px',
overflow: 'hidden',
backgroundColor: '#000',
position: 'relative',
margin: '0 auto 20px auto'
}}>
{isVideoStreaming ? (
<>
<video
ref={videoRef}
autoPlay
playsInline
webkit-playsinline="true"
x-webkit-airplay="allow"
muted
controls={false}
style={{
width: '100%',
height: 'auto',
minHeight: '360px',
display: 'block',
transform: facingMode === 'user' ? 'scaleX(-1)' : 'none',
objectFit: 'cover'
}}
/>
<div style={{
position: 'absolute',
top: '10px',
left: '10px',
background: 'rgba(0, 255, 0, 0.8)',
color: 'white',
padding: '5px 10px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: 'bold'
}}>
🎥 LIVE
</div>
</>
) : (
<div style={{
width: '100%',
height: '360px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: '16px'
}}>
비디오 스트리밍이 꺼져있습니다
</div>
)}
</div>
{/* 에러 메시지 */}
{cameraError && (
<div style={{
padding: '15px',
backgroundColor: '#f8d7da',
borderRadius: '8px',
marginBottom: '20px',
border: '1px solid #f5c6cb'
}}>
<p style={{ margin: 0, color: '#721c24', fontSize: '14px' }}>
{cameraError}
</p>
</div>
)}
{/* 컨트롤 버튼들 */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{!isVideoStreaming ? (
<button
onClick={startVideoStreaming}
style={{
padding: '15px',
fontSize: '16px',
backgroundColor: '#007AFF',
color: 'white',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
비디오 스트리밍 시작
</button>
) : (
<>
<button
onClick={stopVideoStreaming}
style={{
padding: '15px',
fontSize: '16px',
backgroundColor: '#dc3545',
color: 'white',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
⏹️ 비디오 스트리밍 중지
</button>
<button
onClick={switchCamera}
style={{
padding: '15px',
fontSize: '16px',
backgroundColor: '#28a745',
color: 'white',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
🔄 카메라 전환 ({facingMode === 'user' ? '후면으로' : '전면으로'})
</button>
<button
onClick={() => {
if (videoRef.current) {
const video = videoRef.current;
const info = {
'srcObject 존재': !!video.srcObject,
'paused': video.paused,
'readyState': video.readyState,
'networkState': video.networkState,
'videoWidth': video.videoWidth,
'videoHeight': video.videoHeight,
'currentTime': video.currentTime,
'duration': video.duration,
'muted': video.muted,
'autoplay': video.autoplay,
'playsInline': (video as any).playsInline,
};
console.log('비디오 엘리먼트 상태:', info);
alert(JSON.stringify(info, null, 2));
} else {
alert('videoRef.current가 null입니다');
}
}}
style={{
padding: '15px',
fontSize: '16px',
backgroundColor: '#6c757d',
color: 'white',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
🔍 비디오 상태 확인
</button>
</>
)}
</div>
</div>
</>
)
}
export default App