
Complete church management system with bulletin management, media processing, live streaming integration, and web interface. Includes authentication, email notifications, database migrations, and comprehensive test suite.
235 lines
8.2 KiB
HTML
235 lines
8.2 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Netflix-Style Chunk Streaming Test</title>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
margin: 20px;
|
|
background: #111;
|
|
color: #fff;
|
|
}
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
.status {
|
|
background: #333;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
margin: 10px 0;
|
|
font-family: monospace;
|
|
}
|
|
.chunk-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
|
gap: 5px;
|
|
margin: 20px 0;
|
|
}
|
|
.chunk {
|
|
padding: 10px;
|
|
text-align: center;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
}
|
|
.chunk.not-started { background: #666; }
|
|
.chunk.transcoding { background: #f39c12; animation: pulse 1s infinite; }
|
|
.chunk.cached { background: #27ae60; }
|
|
.chunk.failed { background: #e74c3c; }
|
|
.chunk.current { border: 3px solid #fff; }
|
|
@keyframes pulse {
|
|
0% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
100% { opacity: 1; }
|
|
}
|
|
button {
|
|
background: #e50914;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
margin: 5px;
|
|
}
|
|
button:hover { background: #f40612; }
|
|
button:disabled { background: #666; cursor: not-allowed; }
|
|
.controls {
|
|
margin: 20px 0;
|
|
text-align: center;
|
|
}
|
|
.log {
|
|
background: #222;
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
height: 200px;
|
|
overflow-y: auto;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
}
|
|
.success { color: #27ae60; }
|
|
.error { color: #e74c3c; }
|
|
.info { color: #3498db; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>🍿 Netflix-Style Chunk Streaming Test</h1>
|
|
|
|
<div class="status" id="status">
|
|
Click "Load Video Info" to start
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<button onclick="loadVideoInfo()">Load Video Info</button>
|
|
<button onclick="testChunk()" id="testBtn" disabled>Test Random Chunk</button>
|
|
<button onclick="testSequential()" id="seqBtn" disabled>Test Sequential</button>
|
|
<button onclick="clearLog()">Clear Log</button>
|
|
</div>
|
|
|
|
<div class="chunk-grid" id="chunkGrid"></div>
|
|
|
|
<div class="log" id="log"></div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE = window.location.origin;
|
|
const MEDIA_ID = '123e4567-e89b-12d3-a456-426614174000'; // Test UUID
|
|
let videoInfo = null;
|
|
let chunkStatus = {};
|
|
|
|
function log(message, type = 'info') {
|
|
const logDiv = document.getElementById('log');
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
const className = type;
|
|
logDiv.innerHTML += `<div class="${className}">[${timestamp}] ${message}</div>`;
|
|
logDiv.scrollTop = logDiv.scrollHeight;
|
|
}
|
|
|
|
function clearLog() {
|
|
document.getElementById('log').innerHTML = '';
|
|
}
|
|
|
|
function updateStatus(message) {
|
|
document.getElementById('status').textContent = message;
|
|
}
|
|
|
|
async function loadVideoInfo() {
|
|
try {
|
|
log('Loading video info...', 'info');
|
|
const response = await fetch(`${API_BASE}/api/media/stream/${MEDIA_ID}/info`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
|
}
|
|
|
|
videoInfo = await response.json();
|
|
log(`✅ Video loaded: ${videoInfo.total_duration}s, ${videoInfo.num_segments} chunks`, 'success');
|
|
|
|
updateStatus(`Video: ${Math.round(videoInfo.total_duration)}s (${videoInfo.num_segments} chunks of ${videoInfo.segment_duration}s each)`);
|
|
|
|
// Enable buttons
|
|
document.getElementById('testBtn').disabled = false;
|
|
document.getElementById('seqBtn').disabled = false;
|
|
|
|
// Create chunk grid
|
|
createChunkGrid();
|
|
|
|
} catch (error) {
|
|
log(`❌ Failed to load video info: ${error.message}`, 'error');
|
|
updateStatus('Failed to load video info');
|
|
}
|
|
}
|
|
|
|
function createChunkGrid() {
|
|
const grid = document.getElementById('chunkGrid');
|
|
grid.innerHTML = '';
|
|
|
|
for (let i = 0; i < videoInfo.num_segments; i++) {
|
|
const chunk = document.createElement('div');
|
|
chunk.className = 'chunk not-started';
|
|
chunk.textContent = `${i}`;
|
|
chunk.title = `Chunk ${i} (${i * videoInfo.segment_duration}-${(i + 1) * videoInfo.segment_duration}s)`;
|
|
chunk.onclick = () => testSpecificChunk(i);
|
|
grid.appendChild(chunk);
|
|
|
|
chunkStatus[i] = 'not-started';
|
|
}
|
|
}
|
|
|
|
function updateChunkStatus(index, status) {
|
|
chunkStatus[index] = status;
|
|
const chunk = document.querySelector(`#chunkGrid .chunk:nth-child(${index + 1})`);
|
|
if (chunk) {
|
|
chunk.className = `chunk ${status}`;
|
|
}
|
|
}
|
|
|
|
async function testSpecificChunk(index) {
|
|
try {
|
|
updateChunkStatus(index, 'transcoding');
|
|
log(`🎬 Testing chunk ${index}...`, 'info');
|
|
|
|
const startTime = performance.now();
|
|
const response = await fetch(`${API_BASE}/api/media/stream/${MEDIA_ID}/chunk/${index}`);
|
|
const endTime = performance.now();
|
|
|
|
if (response.status === 202) {
|
|
// Still transcoding
|
|
const info = await response.json();
|
|
log(`⏳ Chunk ${index} transcoding (ETA: ${info.estimated_seconds}s)`, 'info');
|
|
|
|
// Poll for completion
|
|
setTimeout(() => testSpecificChunk(index), 2000);
|
|
return;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}`);
|
|
}
|
|
|
|
const contentLength = response.headers.get('content-length');
|
|
const contentType = response.headers.get('content-type');
|
|
const isCached = response.headers.get('x-chunk-cached') === 'true';
|
|
|
|
updateChunkStatus(index, 'cached');
|
|
|
|
log(`✅ Chunk ${index}: ${Math.round(endTime - startTime)}ms, ${contentLength} bytes, ${contentType} ${isCached ? '(cached)' : '(transcoded)'}`, 'success');
|
|
|
|
} catch (error) {
|
|
updateChunkStatus(index, 'failed');
|
|
log(`❌ Chunk ${index} failed: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
async function testChunk() {
|
|
if (!videoInfo) return;
|
|
|
|
const randomIndex = Math.floor(Math.random() * videoInfo.num_segments);
|
|
await testSpecificChunk(randomIndex);
|
|
}
|
|
|
|
let sequentialIndex = 0;
|
|
async function testSequential() {
|
|
if (!videoInfo) return;
|
|
|
|
if (sequentialIndex >= videoInfo.num_segments) {
|
|
log('🎉 Sequential test completed!', 'success');
|
|
sequentialIndex = 0;
|
|
return;
|
|
}
|
|
|
|
await testSpecificChunk(sequentialIndex);
|
|
sequentialIndex++;
|
|
|
|
// Continue after a short delay
|
|
setTimeout(testSequential, 1000);
|
|
}
|
|
|
|
// Load video info on page load
|
|
window.addEventListener('load', loadVideoInfo);
|
|
</script>
|
|
</body>
|
|
</html> |