Skip to content

Commit

Permalink
feat: 비디오/오디오 송출 및 화면공유 테스트용 컴포넌트 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
simeunseo committed Nov 19, 2024
1 parent c91f74e commit eb8f2ff
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 1 deletion.
17 changes: 17 additions & 0 deletions apps/web/src/components/live/AudioPlayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEffect, useRef } from 'react';

import { MediaPlayerProps } from './VideoPlayer';

function AudioPlayer({ stream, muted = false, className = '' }: MediaPlayerProps) {
const audioRef = useRef<HTMLAudioElement>(null);

useEffect(() => {
if (audioRef.current && stream) {
audioRef.current.srcObject = stream;
}
}, [stream]);

return <audio ref={audioRef} autoPlay playsInline muted={muted} className={className} />;
}

export default AudioPlayer;
20 changes: 20 additions & 0 deletions apps/web/src/components/live/VideoPlayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useEffect, useRef } from 'react';

export interface MediaPlayerProps {
stream: MediaStream | null;
muted?: boolean;
className?: string;
}

function VideoPlayer({ stream, muted = true, className = '' }: MediaPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);

useEffect(() => {
if (videoRef.current && stream) {
videoRef.current.srcObject = stream;
}
}, [stream]);

return <video ref={videoRef} autoPlay playsInline muted={muted} className={className} />;
}
export default VideoPlayer;
152 changes: 152 additions & 0 deletions apps/web/src/components/live/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { useEffect, useState } from 'react';

import useMediasoup from '@/hooks/mediasoup/useMediasoup';

import AudioPlayer from './AudioPlayer';
import VideoPlayer from './VideoPlayer';

function MediaContainer() {
const { remoteStreams } = useMediasoup();

// 로컬 스트림 상태 관리
const [localVideoStream, setLocalVideoStream] = useState<MediaStream | null>(null);
const [localAudioStream, setLocalAudioStream] = useState<MediaStream | null>(null);
const [localScreenStream, setLocalScreenStream] = useState<MediaStream | null>(null);
const [isScreenSharing, setIsScreenSharing] = useState(false);

// 스트림 초기화 및 정리를 위한 cleanup 함수들
const cleanupStreams = () => {
if (localVideoStream) {
localVideoStream.getTracks().forEach((track) => track.stop());
setLocalVideoStream(null);
}
if (localAudioStream) {
localAudioStream.getTracks().forEach((track) => track.stop());
setLocalAudioStream(null);
}
if (localScreenStream) {
localScreenStream.getTracks().forEach((track) => track.stop());
setLocalScreenStream(null);
}
};

// 비디오 스트림 초기화
const initVideoStream = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
});
setLocalVideoStream(stream);
} catch (error) {
console.error('Failed to get video stream:', error);
setLocalVideoStream(null);
}
};

// 오디오 스트림 초기화
const initAudioStream = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
});
setLocalAudioStream(stream);
} catch (error) {
console.error('Failed to get audio stream:', error);
setLocalAudioStream(null);
}
};

// 화면 공유 스트림 초기화
const initScreenShare = async () => {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
});

// 화면 공유 종료 이벤트 처리
stream.getVideoTracks()[0].onended = () => {
if (localScreenStream) {
localScreenStream.getTracks().forEach((track) => track.stop());
}
setLocalScreenStream(null);
setIsScreenSharing(false);
};

setLocalScreenStream(stream);
setIsScreenSharing(true);
} catch (error) {
console.error('Failed to start screen sharing:', error);
setLocalScreenStream(null);
setIsScreenSharing(false);
}
};

// 컴포넌트 마운트시 스트림 초기화
useEffect(() => {
const initialize = async () => {
await Promise.all([initVideoStream(), initAudioStream()]);
};

initialize();

// 컴포넌트 언마운트시 cleanup
return () => {
cleanupStreams();
};
}, []);

return (
<div className="grid grid-cols-2 gap-4 p-4">
{/* Local Streams */}
<div className="col-span-2 mb-4">
<div className="relative aspect-video">
{localVideoStream && (
<VideoPlayer
stream={localVideoStream}
muted
className="h-full w-full rounded-lg object-cover"
/>
)}
{localScreenStream && (
<div className="absolute right-0 top-0 aspect-video w-1/4">
<VideoPlayer
stream={localScreenStream}
muted
className="h-full w-full rounded-lg border-2 border-blue-500 object-cover"
/>
</div>
)}
{localAudioStream && <AudioPlayer stream={localAudioStream} muted className="hidden" />}
<div className="bg-black/50 absolute bottom-2 left-2 rounded px-2 py-1 text-sm text-white">
나 (Local)
</div>
{/* Media Controls */}
<div className="absolute bottom-2 right-2 flex gap-2">
<button
onClick={initScreenShare}
className="rounded bg-blue-500 px-3 py-1 text-white hover:bg-blue-600"
disabled={isScreenSharing}
>
{isScreenSharing ? '화면 공유 중' : '화면 공유'}
</button>
</div>
</div>
</div>

{/* Remote Streams */}
{remoteStreams.map((remote, index) => (
<div key={`${remote.socketId}-${index}`} className="relative aspect-video">
{remote.kind === 'video' && (
<VideoPlayer stream={remote.stream} className="h-full w-full rounded-lg object-cover" />
)}
{remote.kind === 'audio' && <AudioPlayer stream={remote.stream} className="hidden" />}
<div className="bg-black/50 absolute bottom-2 left-2 rounded px-2 py-1 text-sm text-white">
{remote.socketId}
</div>
</div>
))}
</div>
);
}

export default MediaContainer;
4 changes: 3 additions & 1 deletion apps/web/src/routes/live/$ticleId.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createFileRoute } from '@tanstack/react-router';

import MediaContainer from '@/components/live';

export const Route = createFileRoute('/live/$ticleId')({
component: RouteComponent,
});

function RouteComponent() {
const { ticleId } = Route.useParams();
return `live ${ticleId}`;
return <MediaContainer />;
}

0 comments on commit eb8f2ff

Please sign in to comment.