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:
giles
2026-02-03 20:23:16 +00:00
parent 5bc655f8c8
commit 86830019ad
24 changed files with 4025 additions and 96 deletions

View File

@@ -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' %}

View File

@@ -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">