Fix GPU encoding black frames and improve debug logging
Some checks are pending
GPU Worker CI/CD / test (push) Waiting to run
GPU Worker CI/CD / deploy (push) Blocked by required conditions

- Add CUDA sync before encoding to ensure RGB->NV12 kernel completes
- Add debug logging for frame data validation (sum check)
- Handle GPUFrame objects in GPUHLSOutput.write()
- Fix cv2.resize for CuPy arrays (use cupyx.scipy.ndimage.zoom)
- Fix fused pipeline parameter ordering (geometric first, color second)
- Add raindrop-style ripple with random position/freq/decay/amp
- Generate final VOD playlist with #EXT-X-ENDLIST

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-04 16:33:12 +00:00
parent b15e381f81
commit 9a8a701492
8 changed files with 471 additions and 37 deletions

View File

@@ -99,10 +99,13 @@ class GPUEncoder:
self._init_frame_buffer()
# Create encoder with low-latency settings (no B-frames for immediate output)
# Use H264 codec explicitly, with SPS/PPS headers for browser compatibility
self.encoder = nvc.CreateEncoder(
width, height, "NV12", usecpuinputbuffer=False,
codec="h264", # Explicit H.264 (not HEVC)
bf=0, # No B-frames - immediate output
lowLatency=1, # Low latency mode
repeatSPSPPS=1, # Include SPS/PPS with each IDR frame
idrPeriod=30, # IDR frame every 30 frames (1 sec at 30fps)
)
# CUDA kernel grid/block config
@@ -189,10 +192,25 @@ class GPUEncoder:
if not frame_gpu.flags['C_CONTIGUOUS']:
frame_gpu = cp.ascontiguousarray(frame_gpu)
# Debug: check input frame has actual data (first few frames only)
if self._frame_count < 3:
frame_sum = float(cp.sum(frame_gpu))
print(f"[GPUEncoder] Frame {self._frame_count}: shape={frame_gpu.shape}, dtype={frame_gpu.dtype}, sum={frame_sum:.0f}", file=sys.stderr)
if frame_sum < 1000:
print(f"[GPUEncoder] WARNING: Frame appears to be mostly black!", file=sys.stderr)
# Convert RGB to NV12 on GPU
kernel = _get_rgb_to_nv12_kernel()
kernel(self._grid, self._block, (frame_gpu, self._y_plane, self._uv_plane, self.width, self.height))
# CRITICAL: Synchronize CUDA to ensure kernel completes before encoding
cp.cuda.Stream.null.synchronize()
# Debug: check Y plane has data after conversion (first few frames only)
if self._frame_count < 3:
y_sum = float(cp.sum(self._y_plane))
print(f"[GPUEncoder] Frame {self._frame_count}: Y plane sum={y_sum:.0f}", file=sys.stderr)
# Encode (GPU to GPU)
result = self.encoder.Encode(self._template_frame)
self._frame_count += 1
@@ -312,6 +330,11 @@ class GPUHLSOutput:
if not self._is_open:
return
# Handle GPUFrame objects (from streaming_gpu primitives)
if hasattr(frame, 'gpu') and hasattr(frame, 'is_on_gpu'):
# It's a GPUFrame - extract the underlying array
frame = frame.gpu if frame.is_on_gpu else frame.cpu
# GPU encode
encoded = self._gpu_encoder.encode_frame(frame)
@@ -439,8 +462,44 @@ class GPUHLSOutput:
self._upload_queue.put(None) # Signal shutdown
self._upload_thread.join(timeout=30)
# Generate final playlist with #EXT-X-ENDLIST for VOD playback
self._generate_final_playlist()
self._gpu_encoder.close()
def _generate_final_playlist(self):
"""Generate final IPFS playlist with #EXT-X-ENDLIST for completed streams."""
with self._upload_lock:
if not self.segment_cids:
return
lines = [
"#EXTM3U",
"#EXT-X-VERSION:3",
f"#EXT-X-TARGETDURATION:{int(self.segment_duration) + 1}",
"#EXT-X-MEDIA-SEQUENCE:0",
"#EXT-X-PLAYLIST-TYPE:VOD", # Mark as VOD for completed streams
]
for seg_num in sorted(self.segment_cids.keys()):
cid = self.segment_cids[seg_num]
lines.append(f"#EXTINF:{self.segment_duration:.3f},")
# Use /ipfs-ts/ path for segments to get correct MIME type (video/mp2t)
segment_gateway = self.ipfs_gateway.replace("/ipfs", "/ipfs-ts")
lines.append(f"{segment_gateway}/{cid}")
# Mark stream as complete - critical for VOD playback
lines.append("#EXT-X-ENDLIST")
playlist_content = "\n".join(lines) + "\n"
# Upload final playlist
self._playlist_cid = self._ipfs_add_bytes(playlist_content.encode(), pin=True)
if self._playlist_cid:
print(f"[GPUHLSOutput] Final VOD playlist: {self._playlist_cid} ({len(self.segment_cids)} segments)", file=sys.stderr)
if self._on_playlist_update:
self._on_playlist_update(self._playlist_cid)
@property
def is_open(self) -> bool:
return self._is_open