Fix live HLS streaming with dynamic quality playlist URLs
Some checks are pending
GPU Worker CI/CD / test (push) Waiting to run
GPU Worker CI/CD / deploy (push) Blocked by required conditions

The problem: HLS.js caches quality playlist URLs from the master playlist.
Even when we update the master playlist CID, HLS.js keeps polling the same
static quality CID URL, so it never sees new segments.

The fix:
- Store quality-level CIDs in database (quality_playlists JSONB column)
- Generate master playlist with dynamic URLs (/runs/{id}/quality/{name}/playlist.m3u8)
- Add quality endpoint that fetches LATEST CID from database
- HLS.js now polls our dynamic endpoints which return fresh content

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-04 21:07:29 +00:00
parent 4647dd52c8
commit 2f56ffc472
4 changed files with 150 additions and 23 deletions

View File

@@ -95,6 +95,7 @@ CREATE TABLE IF NOT EXISTS pending_runs (
actor_id VARCHAR(255),
error TEXT,
ipfs_playlist_cid VARCHAR(128), -- For streaming: IPFS CID of HLS playlist
quality_playlists JSONB, -- For streaming: quality-level playlist CIDs {quality_name: {cid, width, height, bitrate}}
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
@@ -106,6 +107,10 @@ BEGIN
WHERE table_name = 'pending_runs' AND column_name = 'ipfs_playlist_cid') THEN
ALTER TABLE pending_runs ADD COLUMN ipfs_playlist_cid VARCHAR(128);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'pending_runs' AND column_name = 'quality_playlists') THEN
ALTER TABLE pending_runs ADD COLUMN quality_playlists JSONB;
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_pending_runs_status ON pending_runs(status);
@@ -1525,7 +1530,7 @@ async def get_pending_run(run_id: str) -> Optional[dict]:
async with pool.acquire() as conn:
row = await conn.fetchrow(
"""
SELECT run_id, celery_task_id, status, recipe, inputs, dag_json, plan_cid, output_name, actor_id, error, ipfs_playlist_cid, created_at, updated_at
SELECT run_id, celery_task_id, status, recipe, inputs, dag_json, plan_cid, output_name, actor_id, error, ipfs_playlist_cid, quality_playlists, created_at, updated_at
FROM pending_runs WHERE run_id = $1
""",
run_id
@@ -1535,6 +1540,10 @@ async def get_pending_run(run_id: str) -> Optional[dict]:
inputs = row["inputs"]
if isinstance(inputs, str):
inputs = _json.loads(inputs)
# Parse quality_playlists if it's a string
quality_playlists = row.get("quality_playlists")
if isinstance(quality_playlists, str):
quality_playlists = _json.loads(quality_playlists)
return {
"run_id": row["run_id"],
"celery_task_id": row["celery_task_id"],
@@ -1547,6 +1556,7 @@ async def get_pending_run(run_id: str) -> Optional[dict]:
"actor_id": row["actor_id"],
"error": row["error"],
"ipfs_playlist_cid": row["ipfs_playlist_cid"],
"quality_playlists": quality_playlists,
"created_at": row["created_at"].isoformat() if row["created_at"] else None,
"updated_at": row["updated_at"].isoformat() if row["updated_at"] else None,
}
@@ -1632,15 +1642,27 @@ async def update_pending_run_plan(run_id: str, plan_cid: str) -> bool:
return "UPDATE 1" in result
async def update_pending_run_playlist(run_id: str, ipfs_playlist_cid: str) -> bool:
"""Update the IPFS playlist CID of a streaming run."""
async def update_pending_run_playlist(run_id: str, ipfs_playlist_cid: str, quality_playlists: Optional[dict] = None) -> bool:
"""Update the IPFS playlist CID of a streaming run.
Args:
run_id: The run ID
ipfs_playlist_cid: Master playlist CID
quality_playlists: Dict of quality name -> {cid, width, height, bitrate}
"""
if pool is None:
raise RuntimeError("Database pool not initialized - call init_db() first")
async with pool.acquire() as conn:
result = await conn.execute(
"UPDATE pending_runs SET ipfs_playlist_cid = $2, updated_at = NOW() WHERE run_id = $1",
run_id, ipfs_playlist_cid
)
if quality_playlists:
result = await conn.execute(
"UPDATE pending_runs SET ipfs_playlist_cid = $2, quality_playlists = $3, updated_at = NOW() WHERE run_id = $1",
run_id, ipfs_playlist_cid, _json.dumps(quality_playlists)
)
else:
result = await conn.execute(
"UPDATE pending_runs SET ipfs_playlist_cid = $2, updated_at = NOW() WHERE run_id = $1",
run_id, ipfs_playlist_cid
)
return "UPDATE 1" in result