- 텍스트(측점/POI) 전 프레임 사전 계산 Map (requestIdleCallback 백그라운드) - 드론 데이터 이동 평균 스무딩 (smoothFrame ±N프레임) - 30fps→60fps 프레임 간 선형 보간 (performance.now() 기반) - EMA(지수이동평균) 표시 위치 스무딩 (α=0.01 기본값) - 글씨 2배 크기, bold, strokeText 테두리, 배경 박스 제거 - 카메라 파라미터 패널에 smooth/EMA α 슬라이더 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
47 lines
1.3 KiB
TypeScript
47 lines
1.3 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { usePlayerStore } from '../../store/playerStore';
|
|
|
|
interface VideoItem { videoId: string; filename: string; }
|
|
|
|
interface Props {
|
|
onSelect: (videoId: string, filename: string) => void;
|
|
}
|
|
|
|
export default function VideoList({ onSelect }: Props) {
|
|
const [videos, setVideos] = useState<VideoItem[]>([]);
|
|
const { source } = usePlayerStore();
|
|
const activeId = source?.kind === 'server' ? source.videoId : null;
|
|
|
|
useEffect(() => {
|
|
fetch('/api/videos')
|
|
.then((r) => r.json())
|
|
.then(setVideos)
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
if (videos.length === 0) {
|
|
return (
|
|
<div className="text-gray-500 text-sm p-4 text-center">
|
|
서버에 영상이 없습니다
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="divide-y divide-gray-800">
|
|
{videos.map((v) => (
|
|
<button
|
|
key={v.videoId}
|
|
onClick={() => onSelect(v.videoId, v.filename)}
|
|
className={`w-full text-left px-4 py-3 hover:bg-gray-800 transition-colors ${
|
|
activeId === v.videoId ? 'bg-gray-800 border-l-2 border-blue-500' : ''
|
|
}`}
|
|
>
|
|
<div className="text-sm text-white truncate">{v.filename}</div>
|
|
<div className="text-xs text-gray-500 mt-0.5">{v.videoId}</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|