1 . 사용자가 스트리밍을 끝낸 경우 -> 스트림을 삭제할 수 있거나, 스트림이 끝났다는 표시를 하도록
스트리밍이 끝났다고 표시가 되면 , cloudflare API를 호출해서 -> 라이브 스트리밍의 녹화본을 받을 수 있음. -> 그리고 이 스트리밍을 그 녹화본으로 대신할 수 있을 것임.
2 . 채팅 기능 구현 -> supabase의 실시간 기능을 사용 -> 라이브 스트리밍 + 라이브 채팅 기능 구현(라이브 쇼핑처럼)
3 . DB에 현재 활성화된 스트림을 요청하고 , 그것들을 보여주면 된다 -> /live
-> DB로 라이브 중이거나, 녹화된 스트림에 대해 쿼리를 보내면 된다.
첫번째 코드 챌린지 하루 걸렸다
ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
웅... 미소 유지..
먼저, 스트리밍을 할 때 3가지 단계로 나타낼 수 있다.
1 . 스트리밍 하는 중
2 . 스트리밍 시작 전
3 . 스트리밍 끝난 후
이렇게 3단계로 나눌 수 있다고 생각을 해서 화면을 나타낼 수 있는 단계도 3단계로 나타놔주었다.
그럼 어떻게 스트리밍의 상태를 확인할 수 있을까?
이또한 API로 확인할 수 있었다!
checkStreamStatus() 함수를 통해서 GET method를 보내서 stream의 상태를 확인할 수 있었다.
스트리밍 중일 때는 -> connected, 스트리밍이 끝났을 경우 -> disconnected, 나머지 (스트리밍 전) 이렇게 상태를 확인할 수 있다!
checkStreamStatus함수를 받아와서 이런식으로 streamStatus === 'connected'일 때, 'disconnected'일 때, 나머지 일 때 이렇게 상황 별로 사람들에게 UI를 보여줄 수 있다.
여기서, 스트리밍 전일 경우,
요렇게 나타냈다.
이번에는 스트리밍이 끝난 후를 어떻게 할 것인가 생각을 해보도록 하겠다.
스트리밍이 끝난 후, cloudflare에서는 자동으로 녹화본을 저장한다
-> 스트리밍이 끝난 후, 녹화본을 보여주면 좋을 것 같다!
이렇게 streamStatus === 'disconnected' 일 때 ? 보여줄 것들을 <LiveStreamingVideos/> 에 정의해 보았다.
우선 녹화된 video를 가져오기 위해서는 fetch를 이용해서 GET 요청을 보내야 한다.
요렇게 getStreamVideo(stream_id)를 보내서 streamVideo를 받아온다.
streamVideo 중에 최신 비디오를 거른 다음, lastVideo가 있다면 ?
<iframe/> 으로 영상을 사람들에게 보여주는 것이다!
이런식으로 화면 구성을 했으며,
이렇게 나타나게 된다!
그럼 라이브를 시작하게 되면?
이렇게 나타나게 된다!
채팅을 구현하기 전에 먼저
여기 쇼핑에 stream의 영상을 모두 불러오는 것이다.
다만, 스트리밍 중인 영상과, 스트리밍을 하지 않는 영상을 구분해서 불러올 것이다.
상단에 버튼을 추가해서 (?) 구분해볼 수 있도록 한 번 해보도록 하겠다!
우선 기본 상태 (?)이다. 여기서
3가지 (connected, disconnected, null)이 세 상태로 우선 나눠두었다 (근데 null이 안된다아아아아락!!!!!!!!!!)
예를들어 '준비중'을 선택하게 되면
이렇게 상태가 'disconnected' 스트리밍의 목록을 보여주게 된다!
먼저 상태를 선택할 수 있는 <ChooseStatus/> 를 간단하게 살펴보자
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/solid';
import { useState } from 'react';
interface ChooseStatusProps {
onChange: (value: string | null) => void;
}
const statusLabels: { [key: string]: string } = {
connected: '생방송',
disconnected: '녹화본',
null: '준비중',
};
const statusValues: { [key: string]: string | null } = {
생방송: 'connected',
녹화본: 'disconnected',
준비중: null,
};
export default function ChooseStatus({ onChange }: ChooseStatusProps) {
const [status, setStatus] = useState<string>('생방송');
const [click, setClick] = useState(false);
const handleStatusChange = (label: string) => {
const value = statusValues[label];
setStatus(label);
onChange(value);
setClick(false);
};
const handleClick = () => {
setClick(!click);
};
return (
<div className="relative flex flex-col items-start">
<div
className="flex items-center cursor-pointer bg-orange-700 p-2 rounded"
onClick={handleClick}
>
<span className="flex-1 text-sm">{status}</span>
{click ? (
<ChevronUpIcon className="h-5 w-7 pl-3" />
) : (
<ChevronDownIcon className="h-5 w-7 pl-3" />
)}
</div>
{click && (
<ul className="absolute top-full mt-1 bg-orange-600 rounded shadow-lg">
{Object.keys(statusLabels).map((key) => (
<li
key={key}
className="p-2 hover:bg-orange-700 cursor-pointer"
onClick={() => handleStatusChange(statusLabels[key])}
>
{statusLabels[key]}
</li>
))}
</ul>
)}
</div>
);
}
'connected' => 생방송
'disconnected' => 녹화본
null => 준비중
이렇게 세 단계로 영상을 나누고, 사용자에게는 생방송, 녹화본, 준비중 이렇게 보여줘야 하기 때문에 statusLabels, statusValues 를 통해서 선택하고 이를 데이터를 넘겨줄 수 있도록 해주었다.
live.tsx 를 살펴보면,

useEffect()를 사용해서 getStreams() 을 통해 우선 streamData를 모두 불러왔다.
여기서, checkStreamStatus() -> stream의 상태를 확인할 수 있었던 함수
를 통해서 streamStatus를 받아오고, 비디오의 썸네일을 받아오기 위해서 getStreamVideo()함수에서 videoData를 받아왔다.
또한 여기서, return을 통해서 스트림 데이터에 상태와 비디오 썸네일 정보를 추가하고, 최종적으로 streamsWithVideos 배열을 생성하여 상태를 업데이트를 하게된다.
또하나의 useEffect()에서는 stream.status가 selectedStatus와 같은 걸 filter링 처리하고 이 값을 filtered에 저장한 뒤 , setFilteredStreams에 filter된 값을 저장했다.
그럼 selectedStatus는 어디서 가져오냐? --> 바로 ChooseStatus에서 받아온 값을 의미한다!
여기서 onChange로 변경된 값을 setSelectedStatus에 저장하게 되면서 비교가 가능해진 것이다!
또한 useEffect(), useState() 를 사용하기 위해서 'use client'를 적어두었다. 이러한 훅은 클라이언트 사이드에서만 작동하기 때문에, 서버 사이드에서 렌더링되는 경우에는 사용할 수 없다!
고로 actions.ts 폴더를 새로 생성해서 서버에서 불러올 것들으느 따로 지정을 해주었다
'use server';
import db from '@/lib/db';
export async function getStreams() {
const streams = await db.liveStream.findMany({
select: {
created_at: true,
title: true,
description: true,
stream_id: true,
id: true,
user: {
select: {
username: true,
avatar: true,
},
},
},
});
return streams;
}
export async function checkStreamStatus(streamId: string) {
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE_ACCOUNT_ID}/stream/live_inputs/${streamId}`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.CLOUDFLARE_TOKEN}`,
},
}
);
if (!response.ok) {
console.error('Failed to fetch stream status');
return 'unknown';
}
const data = await response.json();
console.log('API 응답 데이터:', data);
if (data && data.result) {
console.log('Stream 상태 데이터:', data.result.status); // 상태 관련 데이터 확인
const status = data.result.status?.current?.state;
return status;
} else {
console.error('No result found in API response:', data);
return 'unknown';
}
}
굳뜨!
와... 준비중이 상태가 null 이 아니라 status : undefinded 였다.. 진짜 눈물이 앞을 가린다.. 원식이가 늘 console로 꼭 찍어보라는 이야기를 정말 많이 했어서 한번 혹시나 싶어서 찍어봤더니..
왕...
하핫....... 이런 대실수를 .....
여기서 null로 찍었던 것들은 result.status이고, 내가 필요한건 현재 state이기 때문에 console.log(status)를 다시 해보니 undefinded가 나왔다.
status === undefined ? null : status 를 사용하여 status가 undefined일 때 null로 변환하도록 했다.
이렇게만 해두었더니, 이전에 코드에서 다 null로 동작하도록 해두었던 것들이 있어서 그런지
웅... 제대로 출력되는 것을 확인할 수 있었다 .. 🥲
해결은 완료했지만 찝찝한 이 무언가.. ㅋㅋㅋㅋㅋㅋㅋㅋㅋ
그래도 해결 완!!!!!!!!!! 😎