기존 GhiVideo 저장소 HEAD의 트래킹 소스 362개 파일을 복제. (node_modules·storage·빌드 산출물·대용량 미디어는 .gitignore 규칙대로 제외) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
64 lines
1.8 KiB
TypeScript
64 lines
1.8 KiB
TypeScript
import React, { useState } from 'react';
|
|
|
|
interface Props {
|
|
videoId: string;
|
|
onConversionDone: () => void;
|
|
}
|
|
|
|
export default function HlsConversionStatus({ videoId, onConversionDone }: Props) {
|
|
const [status, setStatus] = useState<'idle' | 'converting' | 'done' | 'error'>('idle');
|
|
const [percent, setPercent] = useState(0);
|
|
|
|
const startConversion = async () => {
|
|
setStatus('converting');
|
|
await fetch(`/api/hls/${videoId}/convert`, { method: 'POST' });
|
|
|
|
const es = new EventSource(`/api/hls/${videoId}/progress`);
|
|
es.onmessage = (e) => {
|
|
try {
|
|
const data = JSON.parse(e.data);
|
|
setPercent(Math.round(data.percent ?? 0));
|
|
setStatus(data.status);
|
|
if (data.status === 'done') {
|
|
es.close();
|
|
onConversionDone();
|
|
} else if (data.status === 'error') {
|
|
es.close();
|
|
}
|
|
} catch {
|
|
// ignore parse errors
|
|
}
|
|
};
|
|
es.onerror = () => {
|
|
es.close();
|
|
setStatus('error');
|
|
};
|
|
};
|
|
|
|
return (
|
|
<div className="flex items-center gap-2 text-sm">
|
|
{status === 'idle' && (
|
|
<button
|
|
onClick={startConversion}
|
|
className="bg-green-700 hover:bg-green-600 text-white px-3 py-1.5 rounded"
|
|
>
|
|
HLS 변환
|
|
</button>
|
|
)}
|
|
{status === 'converting' && (
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-24 bg-gray-700 rounded-full h-2">
|
|
<div
|
|
className="bg-green-500 h-2 rounded-full transition-all"
|
|
style={{ width: `${percent}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-gray-400">{percent}%</span>
|
|
</div>
|
|
)}
|
|
{status === 'done' && <span className="text-green-400">HLS 준비됨</span>}
|
|
{status === 'error' && <span className="text-red-400">변환 실패</span>}
|
|
</div>
|
|
);
|
|
}
|