1. MediaStream 관련 API
※ 관련 내용 정리한 포스팅 : MediaDevices와 MediaStream을 통해 알아보는 웹 실시간 미디어 처리 방식
2. 구현
※ 예시로 작성한 코드로 일부 내용을 생략한 관계로 참고만 하시길 추천드립니다. ※
2.1 소켓 서버 연결 코드
// socket.ts
import { io } from "socket.io-client";
export const videoChatSocket = io(URL, { path: "/video-server-chat" }); // "/chat" 서버에 연결
2.2 클라이언트 코드
📔 생략한 내용 및 추가할만한 부분
- 미디어 장치 mute 여부를 미디어 장치 변경 시 적용
- 시그널링 서버 연결 및 이벤트 핸들러 등등 주요 로직 context api 로 처리
- 현재 화상 채팅방 나가기 기능 및 기존 스트림 정리
// videoCharRoom.tsx
import { Mic, MicOff, Video, VideoOff } from "lucide-react"; // img 소스
import { useEffect, useRef, useState } from "react";
import { videoChatSocket } from "@/lib/socket";
type MediaType = "audio" | "video";
function VideoCharRoom() {
const [myStream, setMyStream] = useState<MediaStream>(new MediaStream()); // 본인 스트림
const [cameraList, setCameraList] = useState<Map<string, string>>(new Map()); // 현재 카메라 목록
const [micList, setMicList] = useState<Map<string, string>>(new Map()); // 현재 마이크 목록
const [isMicOn, setIsMicOn] = useState(true); // mic on/off 여부
const [isCameraOn, setIsCameraOn] = useState(true); // camera on/off 여부
const [mode, setMode] = useState<"lobby" | "chat">("lobby");
const videoRef = useRef<HTMLVideoElement>(null); // 본인 stream 연결한 video tag ref
// 미디어 장치 초기 설정
useEffect(() => {
(async function init() {
try {
// 초기 미디어 장치 연결 및 목록 가져오기
const initialStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
const devices = await navigator.mediaDevices.enumerateDevices();
const audioDevices = devices.filter(({ kind }) => kind === "audioinput");
const videoDevices = devices.filter(({ kind }) => kind === "videoinput");
const videoDeviceMap = new Map(videoDevices.map(({ deviceId, label }) => [deviceId, label]));
const audioDeviceMap = new Map(audioDevices.map(({ deviceId, label }) => [deviceId, label]));
setCameraList(videoDeviceMap);
setMicList(audioDeviceMap);
setMyStream(initialStream);
} catch (e) {
console.log("미디어 장치를 불러오는데 실패했습니다. :", e);
}
})();
const onConnect = () => console.log("연결됐습니다.");
const onDisconnect = () => console.log("연결이 끊겼습니다.");
const onJoinUser = () => setMode("chat");
// socket 이벤트 등록
videoChatSocket.on("connect", onConnect);
videoChatSocket.on("disconnect", onDisconnect);
videoChatSocket.on("user_joined", onJoinUser);
return () => {
// process 종료시 socket 이벤트 반환
videoChatSocket.off("connect", onConnect);
videoChatSocket.off("disconnect", onDisconnect);
videoChatSocket.off("user_joined", onJoinUser);
};
}, []);
// 본인 stream 연결 video 에 연결
useEffect(() => {
if (!videoRef.current) return;
videoRef.current.srcObject = myStream;
}, [myStream]);
// 현재 stream 의 media track 반환 함수
function getCurrentTrack(stream: MediaStream, type: "audio" | "video") {
const tracks = type === "video" ? stream.getVideoTracks() : stream.getAudioTracks();
return tracks.length > 0 ? tracks[0] : undefined;
}
// 소켓 room 입장 함수
const enterRoom = async () => {
videoChatSocket.emit("enter_room", () => {
setMode("chat");
});
};
// 현재 미디어 장치 mute handle 함수
const handleDeviceMute = (type: MediaType) => {
try {
const track = getCurrentTrack(myStream, type);
if (!track) throw new Error(`No current ${type} track found`);
const mediaState = { video: isCameraOn, audio: isMicOn };
// device mute 여부 토글
if (type === "audio") {
setIsMicOn((prev) => !prev);
mediaState.audio = !isMicOn;
track.enabled = !isMicOn;
} else {
setIsCameraOn((prev) => !prev);
mediaState.video = !isCameraOn;
track.enabled = !isCameraOn;
}
} catch (e) {
console.log(`mute ${type} error`, e);
}
};
// 미디어 장치 변경
async function changeMediaDevice(type: MediaType, deviceId: string) {
try {
myStream.getTracks().forEach((track) => track.stop()); // 기존 트랙 정지
// 반대쪽 트랙 백업
const backupType: MediaType = type === "audio" ? "video" : "audio";
const backupTrack = getCurrentTrack(myStream, backupType);
// MediaStreamConstraints 설정
const constraints: MediaStreamConstraints = { video: false, audio: false };
constraints[type] = { deviceId: { exact: deviceId } };
constraints[backupType] = backupTrack ? backupTrack.getConstraints() : false;
// 변경된 디바이스 정보로 새로운 stream 생성
const selectedStream = await navigator.mediaDevices.getUserMedia(constraints);
// 변경된 camera track 저장
const changedTrack = getCurrentTrack(selectedStream, type);
console.log(changedTrack?.label);
if (!changedTrack) throw new Error(`No changed ${type} track found`);
changedTrack.enabled = type === "audio" ? isMicOn : isCameraOn;
setMyStream(selectedStream);
} catch (e) {
console.log(`change ${type} error`, e);
}
}
return (
<>
{/* 로비 화면 */}
{mode === "lobby" && (
<div className="w-2xl rounded-2xl space-y-3 border p-4">
<h1 className="mb-8">화상 채팅방 로비</h1>
<button
type="button"
className="w-full bg-blue-600 rounded-2xl text-white p-3 cursor-pointer"
onClick={enterRoom}
>
입장
</button>
</div>
)}
{/* 내 video/audio 연결 view */}
{mode === "chat" && (
<div className="size-full flex flex-col">
{/* 내 비디오 스트림 */}
<div className="aspect-video">
<video ref={videoRef} autoPlay playsInline className="size-full object-cover rounded-xl" />
</div>
{/* 미디어 장치 목록 */}
<div className="flex">
{/* 카메라 select */}
<select onChange={(e) => changeMediaDevice("video", e.currentTarget.value)}>
{Array.from(cameraList).map(([deviceId, label]) => (
<option key={deviceId} value={deviceId}>
{label}
</option>
))}
</select>
{/* 마이크 select */}
<select onChange={(e) => changeMediaDevice("audio", e.currentTarget.value)}>
{Array.from(micList).map(([deviceId, label]) => (
<option key={deviceId} value={deviceId}>
{label}
</option>
))}
</select>
{/* 마이크 mute 버튼 */}
<button className="rounded-full size-9 bg-muted" onClick={() => handleDeviceMute("audio")}>
{isMicOn ? <Mic className="size-6" /> : <MicOff className="size-6" />}
</button>
{/* 카메라 mute 버튼 */}
<button className="rounded-full size-9 bg-muted" onClick={() => handleDeviceMute("video")}>
{isCameraOn ? <Video className="size-6" /> : <VideoOff className="size-6" />}
</button>
</div>
</div>
)}
</>
);
}
export default VideoCharRoom;
'websocket & webRTC' 카테고리의 다른 글
4. RTCPeerConnection 간 연결 과정 (offer, answer) (0) | 2025.04.28 |
---|---|
3. RTCPeerConnection 이해와 관련된 주요 인터페이스 (0) | 2025.04.22 |
1. 시그널링 서버 (0) | 2025.04.17 |
0. Web RTC 이해 (0) | 2025.04.16 |
Socket.IO 기본 사용법 (0) | 2025.03.28 |