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: webcam 프록시 설정 및 연결 #23

Merged
merged 3 commits into from
Aug 20, 2024
Merged
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
23 changes: 23 additions & 0 deletions src/entities/slop/model/gonjiam.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export const GONJIAM: ResortInfo = {
top: 'top-[82%]',
left: 'left-[51%]',
},
src: '/api/webcam?url=http://konjiam.live.cdn.cloudn.co.kr/konjiam/cam03.stream/playlist.m3u8',
scale: 1,
},
{
Expand All @@ -109,6 +110,7 @@ export const GONJIAM: ResortInfo = {
top: 'top-[85%]',
left: 'left-[43%]',
},
src: '/api/webcam?url=http://konjiam.live.cdn.cloudn.co.kr/konjiam/cam04.stream/playlist.m3u8',
scale: 1,
},
{
Expand All @@ -119,6 +121,27 @@ export const GONJIAM: ResortInfo = {
left: 'left-[21%]',
},
scale: 1,
src: '/api/webcam?url=http://konjiam.live.cdn.cloudn.co.kr/konjiam/cam02.stream/playlist.m3u8',
},
{
id: 'top-rest-area',
name: '정상 휴게소',
position: {
top: 'top-[5%]',
left: 'left-[19%]',
},
scale: 1,
src: '/api/webcam?url=http://konjiam.live.cdn.cloudn.co.kr/konjiam/cam01.stream/playlist.m3u8',
},
{
id: 'middle-slope',
name: '중간 슬로프',
position: {
top: 'top-[60%]',
left: 'left-[43%]',
},
scale: 1,
src: '/api/webcam?url=http://konjiam.live.cdn.cloudn.co.kr/konjiam/cam05.stream/playlist.m3u8',
},
],
};
1 change: 0 additions & 1 deletion src/entities/slop/model/jisan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ export const JISAN: ResortInfo = {
left: 'left-[29%]',
},
scale: 1,
src: 'http://konjiam.live.cdn.cloudn.co.kr/konjiam/cam01.stream/playlist.m3u8',
},
{
id: 'blue-station',
Expand Down
8 changes: 8 additions & 0 deletions src/entities/slop/model/muju.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ export const MUJU: ResortInfo = {
top: 'top-[78%]',
left: 'left-[47%]',
},
src: '/api/webcam?url=http://muju.live.cdn.cloudn.co.kr/mujuresort/_definst_/cam01.stream/playlist.m3u8',
scale: 1,
},
{
Expand All @@ -254,6 +255,7 @@ export const MUJU: ResortInfo = {
top: 'top-[45%]',
left: 'left-[70%]',
},
src: '/api/webcam?url=http://muju.live.cdn.cloudn.co.kr/mujuresort/_definst_/cam03.stream/playlist.m3u8',
scale: 1,
},
{
Expand All @@ -263,6 +265,7 @@ export const MUJU: ResortInfo = {
top: 'top-[74%]',
left: 'left-[28%]',
},
src: '/api/webcam?url=http://muju.live.cdn.cloudn.co.kr/mujuresort/_definst_/cam05.stream/playlist.m3u8',
scale: 1,
},
{
Expand All @@ -272,6 +275,7 @@ export const MUJU: ResortInfo = {
top: 'top-[65%]',
left: 'left-[68%]',
},
src: '/api/webcam?url=http://muju.live.cdn.cloudn.co.kr/mujuresort/_definst_/cam04.stream/playlist.m3u8',
scale: 1,
},
{
Expand All @@ -281,6 +285,7 @@ export const MUJU: ResortInfo = {
top: 'top-[10%]',
left: 'left-[36%]',
},
src: '/api/webcam?url=http://muju.live.cdn.cloudn.co.kr/mujuresort/_definst_/cam07.stream/playlist.m3u8',
scale: 1,
},
{
Expand All @@ -290,6 +295,7 @@ export const MUJU: ResortInfo = {
top: 'top-[30%]',
left: 'left-[27%]',
},
src: '/api/webcam?url=http://muju.live.cdn.cloudn.co.kr/mujuresort/_definst_/cam08.stream/playlist.m3u8',
scale: 1,
},
{
Expand All @@ -299,6 +305,7 @@ export const MUJU: ResortInfo = {
top: 'top-[15%]',
left: 'left-[23%]',
},
src: '/api/webcam?url=http://muju.live.cdn.cloudn.co.kr/mujuresort/_definst_/cam06.stream/playlist.m3u8',
scale: 1,
},
{
Expand All @@ -308,6 +315,7 @@ export const MUJU: ResortInfo = {
top: 'top-[30%]',
left: 'left-[53%]',
},
src: '/api/webcam?url=http://muju.live.cdn.cloudn.co.kr/mujuresort/_definst_/cam02.stream/playlist.m3u8',
scale: 1,
},
],
Expand Down
32 changes: 32 additions & 0 deletions src/entities/slop/model/yongpyong.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ export const YONGPYONG: ResortInfo = {
top: 'top-[56%]',
left: 'left-[17%]',
},
src: '/api/webcam?url=https://live.yongpyong.co.kr/Ycam1/cam15.stream/chunklist.m3u8',
scale: 1,
},
{
Expand All @@ -205,6 +206,7 @@ export const YONGPYONG: ResortInfo = {
top: 'top-[57%]',
left: 'left-[29%]',
},
src: '/api/webcam?url=https://live.yongpyong.co.kr/Ycam1/cam03.stream/chunklist.m3u8',
scale: 1,
},
{
Expand Down Expand Up @@ -232,6 +234,7 @@ export const YONGPYONG: ResortInfo = {
top: 'top-[2%]',
left: 'left-[61%]',
},
src: '/api/webcam?url=https://live.yongpyong.co.kr/Ycam1/cam05.stream/chunklist.m3u8',
scale: 1,
},
{
Expand All @@ -241,6 +244,7 @@ export const YONGPYONG: ResortInfo = {
top: 'top-[41%]',
left: 'left-[79%]',
},
src: '/api/webcam?url=https://live.yongpyong.co.kr/Ycam1/cam10.stream/chunklist.m3u8',
scale: 1,
},
{
Expand All @@ -250,6 +254,34 @@ export const YONGPYONG: ResortInfo = {
top: 'top-[59%]',
left: 'left-[56%]',
},
src: '/api/webcam?url=https://live.yongpyong.co.kr/Ycam1/cam07.stream/chunklist.m3u8',
scale: 1,
},
{
id: 'base-foreground',
name: '베이스 전경',
position: {
top: 'top-[84%]',
left: 'left-[34%]',
},
scale: 1,
},
{
id: 'access-road',
name: '용평 진입로',
position: {
top: 'top-[86%]',
left: 'left-[26%]',
},
scale: 1,
},
{
id: 'slope-top',
name: '슬로프 정상',
position: {
top: 'top-[26%]',
left: 'left-[30%]',
},
scale: 1,
},
],
Expand Down
10 changes: 10 additions & 0 deletions src/features/slop/ui/slop-camera.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React, { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { toast } from 'sonner';
import type { Position, Webcam } from '@/entities/slop/model/model';
import NeutralFace from '@/shared/icons/neutral-face';
import { cn } from '@/shared/lib';
import CameraButton from '@/shared/ui/cam-button';
import { Tooltip } from '@/shared/ui/tooltip';
Expand Down Expand Up @@ -36,6 +38,14 @@ const SlopCamera = ({

const toggleVideo = () => {
setIsVideoOpen((pre) => !pre);

if (!src) {
toast(
<>
<NeutralFace /> 선택한 웹캠은 아직 준비중 이에요
</>
);
}
};

const { setSelectedSlop } = useSlopStore();
Expand Down
4 changes: 2 additions & 2 deletions src/features/slop/ui/slop-video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ const SlopVideo = ({ src, closeVideo }: SlopVideoProps) => {
return (
<>
<ReactHlsPlayer
className={cn('absolute top-0 z-30 h-full w-full')}
className={cn('absolute top-0 z-50 h-full w-full')}
playerRef={playerRef}
src={src}
autoPlay={true}
controls={false}
/>
<CloseButton className="video-close absolute right-4 top-4 z-[33]" onClick={closeVideo} />
<CloseButton className="video-close absolute right-4 top-4 z-[51]" onClick={closeVideo} />
</>
);
};
Expand Down
58 changes: 58 additions & 0 deletions src/pages/api/webcam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { URL } from 'url';
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { url } = req.query;

if (!url || typeof url !== 'string') {
return res.status(400).json({ error: 'Invalid URL parameter' });
}

const targetUrl = url;

try {
const response = await fetch(targetUrl);

// 응답 헤더 설정
const contentType = response.headers.get('content-type');

if (contentType && contentType.includes('application/vnd.apple.mpegurl')) {
// m3u8 파일 내용 수정
// m3u8의 api 주소를 직접 변경해서 해당 내용 또한 proxy를 통해 요청하도록 수정
const text = await response.text();
const modifiedContent = text.replace(/^(?!#)(.+)$/gm, (match) => {
if (match.startsWith('http')) {
return `/api/webcam?url=${encodeURIComponent(match)}`;
} else {
const fullUrl = new URL(match, targetUrl).toString();
return `/api/webcam?url=${encodeURIComponent(fullUrl)}`;
}
});

res.send(modifiedContent);
} else if (response.body) {
// ReadableStream을 직접 처리
// m3u8이외에 비디오 stream자체를 직접 가져와서 응답
response.body
.pipeTo(
new WritableStream({
write(chunk) {
res.write(chunk);
},
close() {
res.end();
},
})
)
.catch((err) => {
console.error('Stream error:', err);
res.status(500).end();
});
} else {
res.end();
}
} catch (error) {
console.error('Proxy error:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
}
27 changes: 27 additions & 0 deletions src/shared/icons/neutral-face.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { SVGProps } from 'react';
import React from 'react';

interface NeutralFaceProps extends SVGProps<SVGSVGElement> {
className?: string;
}

const NeutralFace = ({ className, ...args }: NeutralFaceProps) => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
{...args}
>
<path
d="M9.12039 9.1199V8.1199C8.5681 8.1199 8.12039 8.56762 8.12039 9.1199H9.12039ZM9.21601 9.1199H10.216C10.216 8.56762 9.7683 8.1199 9.21601 8.1199V9.1199ZM14.8804 9.1199V8.1199C14.3281 8.1199 13.8804 8.56762 13.8804 9.1199H14.8804ZM14.9654 9.1199H15.9654C15.9654 8.56762 15.5177 8.1199 14.9654 8.1199V9.1199ZM9.21601 9.19678V10.1968C9.7683 10.1968 10.216 9.74906 10.216 9.19678H9.21601ZM9.12039 9.19678H8.12039C8.12039 9.74906 8.5681 10.1968 9.12039 10.1968V9.19678ZM14.9654 9.19678V10.1968C15.5177 10.1968 15.9654 9.74906 15.9654 9.19678H14.9654ZM14.8804 9.19678H13.8804C13.8804 9.74906 14.3281 10.1968 14.8804 10.1968V9.19678ZM8.67234 13.9191C8.12006 13.9191 7.67234 14.3668 7.67234 14.9191C7.67234 15.4713 8.12006 15.9191 8.67234 15.9191V13.9191ZM15.3284 15.9191C15.8807 15.9191 16.3284 15.4713 16.3284 14.9191C16.3284 14.3668 15.8807 13.9191 15.3284 13.9191V15.9191ZM20.6004 11.9999C20.6004 16.7495 16.75 20.5999 12.0004 20.5999V22.5999C17.8546 22.5999 22.6004 17.8541 22.6004 11.9999H20.6004ZM12.0004 20.5999C7.25074 20.5999 3.40039 16.7495 3.40039 11.9999H1.40039C1.40039 17.8541 6.14617 22.5999 12.0004 22.5999V20.5999ZM3.40039 11.9999C3.40039 7.25025 7.25074 3.3999 12.0004 3.3999V1.3999C6.14617 1.3999 1.40039 6.14568 1.40039 11.9999H3.40039ZM12.0004 3.3999C16.75 3.3999 20.6004 7.25025 20.6004 11.9999H22.6004C22.6004 6.14568 17.8546 1.3999 12.0004 1.3999V3.3999ZM9.12039 10.1199H9.21601V8.1199H9.12039V10.1199ZM14.8804 10.1199H14.9654V8.1199H14.8804V10.1199ZM8.21601 9.1199V9.19678H10.216V9.1199H8.21601ZM9.21601 8.19678H9.12039V10.1968H9.21601V8.19678ZM10.1204 9.19678V9.1199H8.12039V9.19678H10.1204ZM13.9654 9.1199V9.19678H15.9654V9.1199H13.9654ZM14.9654 8.19678H14.8804V10.1968H14.9654V8.19678ZM15.8804 9.19678V9.1199H13.8804V9.19678H15.8804ZM15.3284 13.9191H8.67234V15.9191H15.3284V13.9191Z"
fill="currentColor"
/>
</svg>
);
};

export default NeutralFace;
4 changes: 2 additions & 2 deletions src/shared/ui/toaster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ const Toaster = ({ ...props }: ToasterProps) => {
toastOptions={{
classNames: {
toast:
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
'group toast group-[.toaster]:bg-[rgba(23,29,35,0.8)] group-[.toaster]:text-white group-[.toaster]:border-border group-[.toaster]:shadow-lg rounded-[16px]',
description: 'group-[.toast]:text-foreground',
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
},
Expand Down
Loading