Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 발화자 하이라이팅 구현 #352

Draft
wants to merge 12 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/web/src/assets/icons/pin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion apps/web/src/components/live/ControlBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ const ControlBar = ({ isOwner, onTicleEnd }: ControlBarProps) => {
{isOpenExitModal && (
<ExitDialog
isOpen={isOpenExitModal}
isOwner={false}
isOwner={isOwner}
handleExit={handleExit}
onClose={onCloseExitModal}
/>
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/components/live/StreamView/List/Pinned.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ interface PinnedListProps {
addPinnedVideo: (stream: StreamData) => void;
removePinnedVideo: () => void;
getAudioMutedState: (stream: StreamData) => boolean;

activeSocketId: string | null;
}

function PinnedGrid({
pinnedVideoStreamData,
removePinnedVideo,
addPinnedVideo,
getAudioMutedState,
activeSocketId,
}: PinnedListProps) {
const { paginatedItems: subPaginatedStreams, ...subPaginationControlsProps } = usePagination({
itemsPerPage: ITEMS_PER_SUB_GRID,
Expand All @@ -38,6 +41,7 @@ function PinnedGrid({
mediaType={pinnedVideoStreamData.consumer?.appData?.mediaTypes}
isMicOn={getAudioMutedState(pinnedVideoStreamData)}
nickname={pinnedVideoStreamData.nickname}
activeSocketId={activeSocketId}
/>
</div>
</div>
Expand All @@ -48,6 +52,7 @@ function PinnedGrid({
videoStreamData={subPaginatedStreams}
onVideoClick={addPinnedVideo}
getAudioMutedState={getAudioMutedState}
activeSocketId={activeSocketId}
/>
</PaginationControls>
</div>
Expand Down
23 changes: 7 additions & 16 deletions apps/web/src/components/live/StreamView/List/SubVideoGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,26 @@
import { cva } from 'class-variance-authority';
import { useEffect, useRef, useState } from 'react';

import { StreamData } from '@/components/live/StreamView';
import VideoPlayer from '@/components/live/StreamView/List/VideoPlayer';

const highlightVariants = cva(`h-full w-full overflow-hidden rounded-lg border-2`, {
variants: {
pinned: {
true: 'border-primary',
false: 'border-transparent',
},
},
defaultVariants: {
pinned: false,
},
});

interface SubVideoGridProps {
videoStreamData: StreamData[];
pinnedVideoStreamData: StreamData | null;
onVideoClick: (stream: StreamData) => void;
getAudioMutedState: (stream: StreamData) => boolean;
activeSocketId: string | null;
}

function SubVideoGrid({
videoStreamData,
pinnedVideoStreamData,
onVideoClick,
getAudioMutedState,
activeSocketId,
}: SubVideoGridProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [videoMaxWidth, setVideoMaxWidth] = useState(0);

useEffect(() => {
// TODO: throttle resize event
const adjustSize = () => {
Expand All @@ -54,9 +44,7 @@ function SubVideoGrid({
<div
key={`${streamData.socketId}${idx}`}
style={{ maxWidth: videoMaxWidth }}
className={highlightVariants({
pinned: pinnedVideoStreamData?.stream?.id === streamData.stream?.id,
})}
className="h-full w-full overflow-hidden rounded-lg"
onClick={() => streamData.stream && onVideoClick(streamData)}
>
<VideoPlayer
Expand All @@ -66,6 +54,9 @@ function SubVideoGrid({
stream={streamData.stream ?? null}
isMicOn={streamData && getAudioMutedState(streamData)}
mediaType={streamData.consumer?.appData?.mediaTypes}
socketId={streamData.socketId}
isPinned={pinnedVideoStreamData?.stream?.id === streamData.stream?.id}
activeSocketId={activeSocketId}
/>
</div>
))}
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/components/live/StreamView/List/UnPinned.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ const ITEMS_PER_GRID = 9;
interface UnPinnedListProps {
addPinnedVideo: (stream: StreamData) => void;
getAudioMutedState: (stream: StreamData) => boolean;
activeSocketId: string | null;
}

function UnPinnedGrid({ addPinnedVideo, getAudioMutedState }: UnPinnedListProps) {
function UnPinnedGrid({ addPinnedVideo, getAudioMutedState, activeSocketId }: UnPinnedListProps) {
const { paginatedItems: paginatedStreams, ...paginationControlsProps } = usePagination({
itemsPerPage: ITEMS_PER_GRID,
});
Expand All @@ -21,6 +22,7 @@ function UnPinnedGrid({ addPinnedVideo, getAudioMutedState }: UnPinnedListProps)
videoStreamData={paginatedStreams}
onVideoClick={addPinnedVideo}
getAudioMutedState={getAudioMutedState}
activeSocketId={activeSocketId}
/>
</PaginationControls>
);
Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/components/live/StreamView/List/VideoGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,15 @@ interface VideoGridProps {
videoStreamData: StreamData[];
onVideoClick: (stream: StreamData) => void;
getAudioMutedState: (stream: StreamData) => boolean;
activeSocketId: string | null;
}

function VideoGrid({ videoStreamData, onVideoClick, getAudioMutedState }: VideoGridProps) {
function VideoGrid({
videoStreamData,
onVideoClick,
getAudioMutedState,
activeSocketId,
}: VideoGridProps) {
return (
<div className={containerVariants({ layout: videoStreamData.length > 3 ? 'grid' : 'flex' })}>
{videoStreamData.map((streamData, idx) => (
Expand All @@ -37,6 +43,8 @@ function VideoGrid({ videoStreamData, onVideoClick, getAudioMutedState }: VideoG
stream={streamData.stream ?? null}
isMicOn={streamData && getAudioMutedState(streamData)}
mediaType={streamData.consumer?.appData?.mediaTypes}
socketId={streamData.socketId}
activeSocketId={activeSocketId}
/>
</div>
))}
Expand Down
58 changes: 43 additions & 15 deletions apps/web/src/components/live/StreamView/List/VideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,31 @@ import { memo, useEffect, useRef, useState } from 'react';

import MicOffIc from '@/assets/icons/mic-off.svg?react';
import MicOnIc from '@/assets/icons/mic-on.svg?react';
import PinIc from '@/assets/icons/pin.svg?react';
import Avatar from '@/components/common/Avatar';
import Badge from '@/components/common/Badge';
import Loading from '@/components/common/Loading';
import cn from '@/utils/cn';

const videoVariants = cva('absolute h-full w-full object-cover transition-opacity duration-300', {
variants: {
loading: {
true: 'opacity-0',
false: 'opacity-100',
const videoVariants = cva(
'absolute h-full w-full rounded-lg object-cover transition-opacity duration-300',
{
variants: {
loading: {
true: 'opacity-0',
false: 'opacity-100',
},
isSpeaking: {
true: 'border-4 border-primary',
false: 'border-4 border-alt',
},
},
},
defaultVariants: {
loading: true,
},
});
defaultVariants: {
loading: true,
isSpeaking: false,
},
}
);

export interface VideoPlayerProps {
stream: MediaStream | null;
Expand All @@ -27,6 +36,9 @@ export interface VideoPlayerProps {
avatarSize?: 'sm' | 'md' | 'lg';
mediaType?: string;
nickname: string;
socketId?: string;
isPinned?: boolean;
activeSocketId: string | null;
}

function VideoPlayer({
Expand All @@ -36,10 +48,15 @@ function VideoPlayer({
isMicOn = false,
avatarSize = 'md',
nickname,
socketId,
isPinned = false,
activeSocketId,
}: VideoPlayerProps) {
const [isLoading, setIsLoading] = useState(true);
const videoRef = useRef<HTMLVideoElement>(null);

const isSpeaking = activeSocketId === socketId;

useEffect(() => {
if (!videoRef.current) return;

Expand All @@ -60,7 +77,10 @@ function VideoPlayer({
autoPlay
playsInline
preload="metadata"
className={videoVariants({ loading: isLoading })}
className={videoVariants({
loading: isLoading,
isSpeaking,
})}
onLoadedData={onLoadedData}
>
<track default kind="captions" srcLang="en" src="SUBTITLE_PATH" />
Expand All @@ -69,7 +89,7 @@ function VideoPlayer({
Your browser does not support the video.
</video>
) : (
<div className={videoVariants({ loading: false })}>
<div className={videoVariants({ loading: false, isSpeaking })}>
<Avatar
size={avatarSize}
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform"
Expand All @@ -92,9 +112,17 @@ function VideoPlayer({
{stream && (
<>
{mediaType === 'video' && (
<div className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full bg-altWeak p-1">
{isMicOn ? <MicOnIc className="text-white" /> : <MicOffIc className="fill-white" />}
</div>
<>
<div className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full bg-altWeak p-1">
{isMicOn ? <MicOnIc className="text-white" /> : <MicOffIc className="fill-white" />}
</div>

{isPinned && (
<div className="absolute left-3 top-3 flex h-8 w-8 items-center justify-center rounded-full bg-primary p-1">
<PinIc className="fill-white" />
</div>
)}
</>
)}
</>
)}
Expand Down
12 changes: 10 additions & 2 deletions apps/web/src/components/live/StreamView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { types } from 'mediasoup-client';
import { MediaTypes } from '@repo/mediasoup';

import AudioStreams from '@/components/live/StreamView/AudioStreams';
import PinnedGrid from '@/components/live/StreamView/List/Pinned';
import UnPinnedGrid from '@/components/live/StreamView/List/UnPinned';
import useAudioLevelDetector from '@/hooks/mediasoup/useAudioLevelDetector';
import useAudioState from '@/hooks/useAudioState';
import usePinnedVideo from '@/hooks/usePinnedVideo';

import AudioStreams from './AudioStreams';

export interface StreamData {
socketId: string;
nickname: string;
Expand All @@ -19,6 +21,7 @@ export interface StreamData {
const StreamView = () => {
const { pinnedVideoStreamData, removePinnedVideo, selectPinnedVideo } = usePinnedVideo();
const { getAudioMutedState } = useAudioState();
const { activeSocketId } = useAudioLevelDetector();

return (
<div className="relative flex h-full flex-1 items-center justify-center pt-8">
Expand All @@ -28,9 +31,14 @@ const StreamView = () => {
addPinnedVideo={selectPinnedVideo}
removePinnedVideo={removePinnedVideo}
getAudioMutedState={getAudioMutedState}
activeSocketId={activeSocketId}
/>
) : (
<UnPinnedGrid addPinnedVideo={selectPinnedVideo} getAudioMutedState={getAudioMutedState} />
<UnPinnedGrid
addPinnedVideo={selectPinnedVideo}
getAudioMutedState={getAudioMutedState}
activeSocketId={activeSocketId}
/>
)}
<AudioStreams />
</div>
Expand Down
Loading
Loading