초기 커밋: 스테이션(측점) 기반 주행영상 플레이어
- 클라이언트(React/Vite): Video.js 플레이어, 하단 스테이션바, POI/구조물 영상 오버레이, 카메라 파라미터 보정, 미니맵/RoutePanel - 서버(Express): Range 스트리밍, HLS 변환, 프레임 추출, tus 업로드, 주석 API - 최근 작업: 스테이션바 종점역 표출/양끝 정렬/미도착(재생불가) 표시, POI 라벨 '구분' 우선·컴팩트 팝업·겹침제외 토글·동일좌표 다중행, 진행방향(상/하) 우선 표출, 커서 배지 크기·픽셀 떨림 개선 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import type Player from 'video.js/dist/types/player';
|
||||
|
||||
interface KeyboardOptions {
|
||||
playerRef: React.RefObject<Player | null>;
|
||||
onStepForward: () => void;
|
||||
onStepBackward: () => void;
|
||||
onCaptureFrame: () => void;
|
||||
onAddMemo: () => void;
|
||||
onToggleHelp?: () => void;
|
||||
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
export function useKeyboard({
|
||||
playerRef,
|
||||
onStepForward,
|
||||
onStepBackward,
|
||||
onCaptureFrame,
|
||||
onAddMemo,
|
||||
onToggleHelp,
|
||||
containerRef,
|
||||
}: KeyboardOptions) {
|
||||
const handleKey = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
// Don't fire when typing in an input
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) return;
|
||||
|
||||
const player = playerRef.current;
|
||||
if (!player) return;
|
||||
|
||||
const ct = player.currentTime() ?? 0;
|
||||
const dur = player.duration() ?? 0;
|
||||
|
||||
switch (e.code) {
|
||||
case 'Space':
|
||||
e.preventDefault();
|
||||
player.paused() ? player.play() : player.pause();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
player.currentTime(Math.max(0, ct - 5));
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
player.currentTime(Math.min(dur, ct + 5));
|
||||
break;
|
||||
case 'KeyJ':
|
||||
player.currentTime(Math.max(0, ct - 10));
|
||||
break;
|
||||
case 'KeyL':
|
||||
player.currentTime(Math.min(dur, ct + 10));
|
||||
break;
|
||||
case 'Comma':
|
||||
e.preventDefault();
|
||||
onStepBackward();
|
||||
break;
|
||||
case 'Period':
|
||||
e.preventDefault();
|
||||
onStepForward();
|
||||
break;
|
||||
case 'BracketLeft':
|
||||
player.currentTime(Math.max(0, ct - 30));
|
||||
break;
|
||||
case 'BracketRight':
|
||||
player.currentTime(Math.min(dur, ct + 30));
|
||||
break;
|
||||
case 'KeyF':
|
||||
if (containerRef.current) {
|
||||
if (!document.fullscreenElement) {
|
||||
containerRef.current.requestFullscreen();
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'KeyM':
|
||||
if (e.shiftKey) {
|
||||
e.preventDefault();
|
||||
onAddMemo();
|
||||
} else {
|
||||
player.muted(!player.muted());
|
||||
}
|
||||
break;
|
||||
case 'Equal':
|
||||
case 'NumpadAdd':
|
||||
player.playbackRate(Math.min(4, (player.playbackRate() ?? 1) + 0.25));
|
||||
break;
|
||||
case 'Minus':
|
||||
case 'NumpadSubtract':
|
||||
player.playbackRate(Math.max(0.25, (player.playbackRate() ?? 1) - 0.25));
|
||||
break;
|
||||
case 'KeyS':
|
||||
if (e.shiftKey) { e.preventDefault(); onCaptureFrame(); }
|
||||
break;
|
||||
case 'Slash':
|
||||
if (e.shiftKey) { e.preventDefault(); onToggleHelp?.(); }
|
||||
break;
|
||||
default:
|
||||
if (e.code >= 'Digit0' && e.code <= 'Digit9') {
|
||||
const pct = parseInt(e.code.replace('Digit', ''), 10) / 10;
|
||||
player.currentTime(dur * pct);
|
||||
}
|
||||
}
|
||||
},
|
||||
[playerRef, onStepForward, onStepBackward, onCaptureFrame, onAddMemo, onToggleHelp, containerRef]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKey);
|
||||
return () => window.removeEventListener('keydown', handleKey);
|
||||
}, [handleKey]);
|
||||
}
|
||||
Reference in New Issue
Block a user