Fix live HLS streaming with dynamic quality playlist URLs
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:
36
database.py
36
database.py
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user