Make HLS segment uploads async to prevent stuttering
Some checks are pending
GPU Worker CI/CD / test (push) Waiting to run
GPU Worker CI/CD / deploy (push) Blocked by required conditions

This commit is contained in:
giles
2026-02-04 10:25:56 +00:00
parent dd169635ca
commit ef3638d3cf

View File

@@ -8,6 +8,8 @@ Frames stay on GPU throughout: CuPy → NV12 conversion → NVENC encoding.
import numpy as np import numpy as np
import subprocess import subprocess
import sys import sys
import threading
import queue
from pathlib import Path from pathlib import Path
from typing import Tuple, Optional, Union from typing import Tuple, Optional, Union
import time import time
@@ -211,6 +213,7 @@ class GPUHLSOutput:
GPU-accelerated HLS output with IPFS upload. GPU-accelerated HLS output with IPFS upload.
Uses zero-copy GPU encoding and writes HLS segments. Uses zero-copy GPU encoding and writes HLS segments.
Uploads happen asynchronously in a background thread to avoid stuttering.
""" """
def __init__( def __init__(
@@ -245,12 +248,18 @@ class GPUHLSOutput:
# Track segment CIDs for IPFS # Track segment CIDs for IPFS
self.segment_cids = {} self.segment_cids = {}
self._playlist_cid = None self._playlist_cid = None
self._upload_lock = threading.Lock()
# Import IPFS client # Import IPFS client
from ipfs_client import add_file, add_bytes from ipfs_client import add_file, add_bytes
self._ipfs_add_file = add_file self._ipfs_add_file = add_file
self._ipfs_add_bytes = add_bytes self._ipfs_add_bytes = add_bytes
# Background upload thread
self._upload_queue = queue.Queue()
self._upload_thread = threading.Thread(target=self._upload_worker, daemon=True)
self._upload_thread.start()
# Setup ffmpeg for muxing (takes raw H.264, outputs .ts segments) # Setup ffmpeg for muxing (takes raw H.264, outputs .ts segments)
self._setup_muxer() self._setup_muxer()
@@ -303,51 +312,76 @@ class GPUHLSOutput:
self._frames_in_segment = 0 self._frames_in_segment = 0
self._check_upload_segments() self._check_upload_segments()
def _upload_worker(self):
"""Background worker thread for async IPFS uploads."""
while True:
try:
item = self._upload_queue.get(timeout=1.0)
if item is None: # Shutdown signal
break
seg_path, seg_num = item
self._do_upload(seg_path, seg_num)
except queue.Empty:
continue
except Exception as e:
print(f"Upload worker error: {e}", file=sys.stderr)
def _do_upload(self, seg_path: Path, seg_num: int):
"""Actually perform the upload (runs in background thread)."""
try:
cid = self._ipfs_add_file(seg_path, pin=True)
if cid:
with self._upload_lock:
self.segment_cids[seg_num] = cid
print(f"Added to IPFS: {seg_path.name} -> {cid}", file=sys.stderr)
self._update_playlist()
except Exception as e:
print(f"Failed to add to IPFS: {e}", file=sys.stderr)
def _check_upload_segments(self): def _check_upload_segments(self):
"""Check for and upload new segments to IPFS.""" """Check for and queue new segments for async IPFS upload."""
segments = sorted(self.output_dir.glob("segment_*.ts")) segments = sorted(self.output_dir.glob("segment_*.ts"))
for seg_path in segments: for seg_path in segments:
seg_num = int(seg_path.stem.split("_")[1]) seg_num = int(seg_path.stem.split("_")[1])
if seg_num in self.segment_cids: with self._upload_lock:
continue if seg_num in self.segment_cids:
continue
# Check if segment is complete # Check if segment is complete (quick check, no blocking)
try: try:
size1 = seg_path.stat().st_size size1 = seg_path.stat().st_size
if size1 == 0: if size1 == 0:
continue continue
time.sleep(0.05) # Quick non-blocking check
time.sleep(0.01)
size2 = seg_path.stat().st_size size2 = seg_path.stat().st_size
if size1 != size2: if size1 != size2:
continue continue
except FileNotFoundError: except FileNotFoundError:
continue continue
# Upload to IPFS # Queue for async upload (non-blocking!)
cid = self._ipfs_add_file(seg_path, pin=True) self._upload_queue.put((seg_path, seg_num))
if cid:
self.segment_cids[seg_num] = cid
print(f"Added to IPFS: {seg_path.name} -> {cid}", file=sys.stderr)
self._update_playlist()
def _update_playlist(self): def _update_playlist(self):
"""Generate and upload IPFS-aware playlist.""" """Generate and upload IPFS-aware playlist."""
if not self.segment_cids: with self._upload_lock:
return if not self.segment_cids:
return
lines = [ lines = [
"#EXTM3U", "#EXTM3U",
"#EXT-X-VERSION:3", "#EXT-X-VERSION:3",
f"#EXT-X-TARGETDURATION:{int(self.segment_duration) + 1}", f"#EXT-X-TARGETDURATION:{int(self.segment_duration) + 1}",
"#EXT-X-MEDIA-SEQUENCE:0", "#EXT-X-MEDIA-SEQUENCE:0",
] ]
for seg_num in sorted(self.segment_cids.keys()): for seg_num in sorted(self.segment_cids.keys()):
cid = self.segment_cids[seg_num] cid = self.segment_cids[seg_num]
lines.append(f"#EXTINF:{self.segment_duration:.3f},") lines.append(f"#EXTINF:{self.segment_duration:.3f},")
lines.append(f"{self.ipfs_gateway}/{cid}") lines.append(f"{self.ipfs_gateway}/{cid}")
playlist_content = "\n".join(lines) + "\n" playlist_content = "\n".join(lines) + "\n"
@@ -381,6 +415,10 @@ class GPUHLSOutput:
# Final segment upload # Final segment upload
self._check_upload_segments() self._check_upload_segments()
# Wait for pending uploads to complete
self._upload_queue.put(None) # Signal shutdown
self._upload_thread.join(timeout=30)
self._gpu_encoder.close() self._gpu_encoder.close()
@property @property