Add IPFS HLS streaming and GPU optimizations
- Add IPFSHLSOutput class that uploads segments to IPFS as they're created - Update streaming task to use IPFS HLS output for distributed streaming - Add /ipfs-stream endpoint to get IPFS playlist URL - Update /stream endpoint to redirect to IPFS when available - Add GPU persistence mode (STREAMING_GPU_PERSIST=1) to keep frames on GPU - Add hardware video decoding (NVDEC) support for faster video processing - Add GPU-accelerated primitive libraries: blending_gpu, color_ops_gpu, geometry_gpu - Add streaming_gpu module with GPUFrame class for tracking CPU/GPU data location - Add Dockerfile.gpu for building GPU-enabled worker image Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
15
app/templates/cache/detail.html
vendored
15
app/templates/cache/detail.html
vendored
@@ -13,17 +13,32 @@
|
||||
<!-- Preview -->
|
||||
<div class="bg-gray-800 rounded-lg border border-gray-700 mb-6 overflow-hidden">
|
||||
{% if cache.mime_type and cache.mime_type.startswith('image/') %}
|
||||
{% if cache.remote_only and cache.ipfs_cid %}
|
||||
<img src="https://ipfs.io/ipfs/{{ cache.ipfs_cid }}" alt=""
|
||||
class="w-full max-h-96 object-contain bg-gray-900">
|
||||
{% else %}
|
||||
<img src="/cache/{{ cache.cid }}/raw" alt=""
|
||||
class="w-full max-h-96 object-contain bg-gray-900">
|
||||
{% endif %}
|
||||
|
||||
{% elif cache.mime_type and cache.mime_type.startswith('video/') %}
|
||||
{% if cache.remote_only and cache.ipfs_cid %}
|
||||
<video src="https://ipfs.io/ipfs/{{ cache.ipfs_cid }}" controls
|
||||
class="w-full max-h-96 bg-gray-900">
|
||||
</video>
|
||||
{% else %}
|
||||
<video src="/cache/{{ cache.cid }}/raw" controls
|
||||
class="w-full max-h-96 bg-gray-900">
|
||||
</video>
|
||||
{% endif %}
|
||||
|
||||
{% elif cache.mime_type and cache.mime_type.startswith('audio/') %}
|
||||
<div class="p-8 bg-gray-900">
|
||||
{% if cache.remote_only and cache.ipfs_cid %}
|
||||
<audio src="https://ipfs.io/ipfs/{{ cache.ipfs_cid }}" controls class="w-full"></audio>
|
||||
{% else %}
|
||||
<audio src="/cache/{{ cache.cid }}/raw" controls class="w-full"></audio>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% elif cache.mime_type == 'application/json' %}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.23.0/cytoscape.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.5/dagre.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/cytoscape-dagre@2.5.0/cytoscape-dagre.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.4.12/dist/hls.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -73,6 +74,174 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Stream Player (shown during rendering) -->
|
||||
{% if run.status == 'rendering' %}
|
||||
<div id="live-stream-container" class="mb-6 bg-gray-800 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold flex items-center">
|
||||
<span class="w-3 h-3 bg-red-500 rounded-full mr-2 animate-pulse"></span>
|
||||
Live Preview
|
||||
</h3>
|
||||
<div id="stream-status" class="text-sm text-gray-400">Connecting...</div>
|
||||
</div>
|
||||
<div class="relative bg-black rounded-lg overflow-hidden" style="aspect-ratio: 16/9;">
|
||||
<video id="live-video" class="w-full h-full" controls autoplay muted playsinline></video>
|
||||
<div id="stream-loading" class="absolute inset-0 flex items-center justify-center bg-gray-900/80">
|
||||
<div class="text-center">
|
||||
<div class="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full mx-auto mb-2"></div>
|
||||
<div class="text-gray-400">Waiting for stream...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-500">
|
||||
Stream URL: <code class="bg-gray-900 px-1 rounded">/runs/{{ run.run_id }}/hls/stream.m3u8</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const video = document.getElementById('live-video');
|
||||
const statusEl = document.getElementById('stream-status');
|
||||
const loadingEl = document.getElementById('stream-loading');
|
||||
const hlsUrl = '/runs/{{ run.run_id }}/hls/stream.m3u8';
|
||||
let hls = null;
|
||||
let retryCount = 0;
|
||||
const maxRetries = 120; // Try for up to 4 minutes
|
||||
let segmentsLoaded = 0;
|
||||
|
||||
function initHls() {
|
||||
if (Hls.isSupported()) {
|
||||
hls = new Hls({
|
||||
// Stability over low latency - buffer more for smoother playback
|
||||
liveSyncDurationCount: 4, // Stay 4 segments behind live edge
|
||||
liveMaxLatencyDurationCount: 8, // Max 8 segments behind
|
||||
liveDurationInfinity: true, // Treat as infinite live stream
|
||||
|
||||
// Large buffers to absorb rendering speed variations
|
||||
maxBufferLength: 60, // Buffer up to 60s ahead
|
||||
maxMaxBufferLength: 120, // Allow even more if needed
|
||||
maxBufferSize: 60 * 1024 * 1024, // 60MB buffer
|
||||
maxBufferHole: 0.5, // Tolerate small gaps
|
||||
|
||||
// Back buffer for smooth seeking
|
||||
backBufferLength: 30,
|
||||
|
||||
// Playlist reload settings
|
||||
manifestLoadingTimeOut: 10000,
|
||||
manifestLoadingMaxRetry: 4,
|
||||
levelLoadingTimeOut: 10000,
|
||||
levelLoadingMaxRetry: 4,
|
||||
fragLoadingTimeOut: 20000,
|
||||
fragLoadingMaxRetry: 6,
|
||||
|
||||
// Start at lowest quality for faster start
|
||||
startLevel: 0,
|
||||
|
||||
// Enable smooth level switching
|
||||
abrEwmaDefaultEstimate: 500000,
|
||||
});
|
||||
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, function(event, data) {
|
||||
loadingEl.classList.add('hidden');
|
||||
statusEl.textContent = 'Buffering...';
|
||||
statusEl.classList.remove('text-gray-400');
|
||||
statusEl.classList.add('text-yellow-400');
|
||||
video.play().catch(() => {});
|
||||
});
|
||||
|
||||
hls.on(Hls.Events.FRAG_LOADED, function(event, data) {
|
||||
retryCount = 0;
|
||||
segmentsLoaded++;
|
||||
statusEl.textContent = `Playing (${segmentsLoaded} segments)`;
|
||||
statusEl.classList.remove('text-yellow-400', 'text-gray-400');
|
||||
statusEl.classList.add('text-green-400');
|
||||
});
|
||||
|
||||
hls.on(Hls.Events.BUFFER_APPENDED, function() {
|
||||
// Hide loading once we have buffered content
|
||||
loadingEl.classList.add('hidden');
|
||||
});
|
||||
|
||||
hls.on(Hls.Events.ERROR, function(event, data) {
|
||||
console.log('HLS error:', data.type, data.details, data.fatal);
|
||||
|
||||
if (data.fatal) {
|
||||
switch (data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
if (retryCount < maxRetries) {
|
||||
retryCount++;
|
||||
statusEl.textContent = `Waiting for stream... (${retryCount})`;
|
||||
statusEl.classList.remove('text-green-400');
|
||||
statusEl.classList.add('text-yellow-400');
|
||||
// Exponential backoff with jitter
|
||||
const delay = Math.min(1000 * Math.pow(1.5, Math.min(retryCount, 6)), 10000);
|
||||
setTimeout(() => {
|
||||
hls.loadSource(hlsUrl);
|
||||
}, delay + Math.random() * 1000);
|
||||
} else {
|
||||
statusEl.textContent = 'Stream unavailable';
|
||||
statusEl.classList.add('text-red-400');
|
||||
}
|
||||
break;
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
console.log('Media error, attempting recovery');
|
||||
hls.recoverMediaError();
|
||||
break;
|
||||
default:
|
||||
statusEl.textContent = 'Stream error';
|
||||
statusEl.classList.add('text-red-400');
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Non-fatal error - just log it
|
||||
if (data.details === 'bufferStalledError') {
|
||||
statusEl.textContent = 'Buffering...';
|
||||
statusEl.classList.remove('text-green-400');
|
||||
statusEl.classList.add('text-yellow-400');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle video stalls
|
||||
video.addEventListener('waiting', function() {
|
||||
statusEl.textContent = 'Buffering...';
|
||||
statusEl.classList.remove('text-green-400');
|
||||
statusEl.classList.add('text-yellow-400');
|
||||
});
|
||||
|
||||
video.addEventListener('playing', function() {
|
||||
statusEl.textContent = `Playing (${segmentsLoaded} segments)`;
|
||||
statusEl.classList.remove('text-yellow-400');
|
||||
statusEl.classList.add('text-green-400');
|
||||
});
|
||||
|
||||
hls.loadSource(hlsUrl);
|
||||
hls.attachMedia(video);
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
// Native HLS support (Safari)
|
||||
video.src = hlsUrl;
|
||||
video.addEventListener('loadedmetadata', function() {
|
||||
loadingEl.classList.add('hidden');
|
||||
statusEl.textContent = 'Playing';
|
||||
video.play().catch(() => {});
|
||||
});
|
||||
} else {
|
||||
statusEl.textContent = 'HLS not supported';
|
||||
statusEl.classList.add('text-red-400');
|
||||
}
|
||||
}
|
||||
|
||||
// Start trying to connect
|
||||
initHls();
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (hls) hls.destroy();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="border-b border-gray-700 mb-6">
|
||||
<nav class="flex space-x-8">
|
||||
|
||||
Reference in New Issue
Block a user