초기 커밋: DefVideo 소스 등록

abcVideo 플레이어 소스 (client / server / shared / pythonsource / docs / .claude).
.gitignore 적용으로 node_modules·storage·samplevideo·미디어 등 대용량 일괄 제외.
103 files, ~964K.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 03:20:27 +00:00
commit 82662d417d
103 changed files with 17213 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
import { useEffect, useRef, useCallback } from 'react';
import videojs from 'video.js';
import type Player from 'video.js/dist/types/player';
import Hls from 'hls.js';
import { usePlayerStore } from '../store/playerStore';
const HLS_CONFIG = {
maxBufferLength: 30,
maxMaxBufferLength: 600,
maxBufferSize: 60 * 1024 * 1024,
backBufferLength: 30,
enableWorker: true,
};
export function useVideoPlayer(containerRef: React.RefObject<HTMLDivElement | null>) {
const playerRef = useRef<Player | null>(null);
const hlsRef = useRef<Hls | null>(null);
const store = usePlayerStore();
useEffect(() => {
if (!containerRef.current || playerRef.current) return;
const videoEl = document.createElement('video-js');
videoEl.classList.add('vjs-big-play-centered', 'vjs-fluid');
containerRef.current.appendChild(videoEl);
const player = videojs(videoEl, {
controls: true,
fluid: true,
responsive: true,
playbackRates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 4],
html5: { vhs: { overrideNative: true } },
});
player.on('play', () => store.setPlaying(true));
player.on('pause', () => store.setPlaying(false));
player.on('timeupdate', () => store.setCurrentTime(player.currentTime() ?? 0));
player.on('durationchange', () => store.setDuration(player.duration() ?? 0));
player.on('volumechange', () => {
store.setVolume(player.volume() ?? 1);
store.setMuted(player.muted() ?? false);
});
player.on('ratechange', () => store.setPlaybackRate(player.playbackRate() ?? 1));
playerRef.current = player;
return () => {
hlsRef.current?.destroy();
hlsRef.current = null;
if (playerRef.current && !playerRef.current.isDisposed()) {
playerRef.current.dispose();
playerRef.current = null;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const loadLocalFile = useCallback((file: File) => {
const player = playerRef.current;
if (!player) return;
// Clean up previous hls
hlsRef.current?.destroy();
hlsRef.current = null;
const objectUrl = URL.createObjectURL(file);
player.src({ src: objectUrl, type: file.type || 'video/mp4' });
store.setSource({ kind: 'local', file, objectUrl });
store.setHlsReady(false);
// Revoke objectUrl when video element is reset
player.one('emptied', () => {
URL.revokeObjectURL(objectUrl);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const loadServerStream = useCallback((videoId: string, filename: string) => {
const player = playerRef.current;
if (!player) return;
hlsRef.current?.destroy();
hlsRef.current = null;
// Immediate playback via Range Request
const streamUrl = `/api/stream/${videoId}`;
player.src({ src: streamUrl, type: 'video/mp4' });
store.setSource({ kind: 'server', videoId, filename });
store.setHlsReady(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const switchToHls = useCallback((videoId: string) => {
const player = playerRef.current;
if (!player) return;
const hlsId = videoId.replace(/\.[^.]+$/, '');
const hlsUrl = `/api/hls/${hlsId}/index.m3u8`;
const savedTime = player.currentTime() ?? 0;
if (Hls.isSupported()) {
const hls = new Hls(HLS_CONFIG);
hls.loadSource(hlsUrl);
const videoEl = player.tech(true)?.el() as HTMLVideoElement;
hls.attachMedia(videoEl);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
player.currentTime(savedTime);
hlsRef.current = hls;
store.setHlsReady(true);
});
} else {
player.src({ src: hlsUrl, type: 'application/x-mpegURL' });
player.currentTime(savedTime);
store.setHlsReady(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const getVideoElement = useCallback((): HTMLVideoElement | null => {
return (playerRef.current?.tech(true)?.el() as HTMLVideoElement | null) ?? null;
}, []);
return { playerRef, loadLocalFile, loadServerStream, switchToHls, getVideoElement };
}