Spaces:
Running
Running
Upload dvr.py with huggingface_hub
Browse files
dvr.py
CHANGED
|
@@ -60,10 +60,12 @@ def _start_buffer(channel_id, stream_url):
|
|
| 60 |
"-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "5",
|
| 61 |
"-i", stream_url,
|
| 62 |
"-c", "copy", # copy, không transcode -> nhẹ CPU
|
|
|
|
| 63 |
"-f", "hls",
|
| 64 |
"-hls_time", str(SEG_SEC),
|
| 65 |
"-hls_list_size", str(LIST_SIZE), # giữ ~5 phút
|
| 66 |
-
"-hls_flags", "delete_segments+omit_endlist+independent_segments",
|
|
|
|
| 67 |
"-hls_segment_filename", seg,
|
| 68 |
playlist,
|
| 69 |
]
|
|
@@ -102,16 +104,23 @@ def buffer_playlist(channel_id: str):
|
|
| 102 |
if not os.path.exists(pl):
|
| 103 |
return JSONResponse({"error": "buffer not ready"}, status_code=404)
|
| 104 |
txt = open(pl, "r", encoding="utf-8", errors="ignore").read()
|
| 105 |
-
# rewrite
|
| 106 |
out = []
|
| 107 |
for line in txt.split("\n"):
|
| 108 |
s = line.strip()
|
| 109 |
if s and not s.startswith("#") and s.endswith(".ts"):
|
|
|
|
| 110 |
out.append(f"/api/dvr/buffer/seg/{channel_id}/{s}")
|
| 111 |
else:
|
| 112 |
out.append(line)
|
|
|
|
| 113 |
return Response("\n".join(out), media_type="application/vnd.apple.mpegurl",
|
| 114 |
-
headers={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
@router.get("/api/dvr/buffer/seg/{channel_id}/{seg}")
|
| 117 |
def buffer_seg(channel_id: str, seg: str):
|
|
@@ -202,15 +211,40 @@ _SCHEDULES = _load_sched()
|
|
| 202 |
|
| 203 |
@router.post("/api/dvr/record/schedule")
|
| 204 |
def record_schedule(payload: dict = Body(...)):
|
| 205 |
-
"""body: {channel, start_iso (giờ VN), duration_sec, prebuffer?, upload_repo?}"""
|
| 206 |
ch = str(payload.get("channel", "")).lower().strip()
|
| 207 |
if not ch:
|
| 208 |
return JSONResponse({"error": "missing channel"}, status_code=400)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
job = {
|
| 210 |
"id": str(int(time.time() * 1000)),
|
| 211 |
"channel": ch,
|
| 212 |
-
"start_iso":
|
| 213 |
-
"
|
|
|
|
| 214 |
"prebuffer": bool(payload.get("prebuffer", True)),
|
| 215 |
"upload_repo": payload.get("upload_repo"),
|
| 216 |
"status": "pending", "file": None,
|
|
|
|
| 60 |
"-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "5",
|
| 61 |
"-i", stream_url,
|
| 62 |
"-c", "copy", # copy, không transcode -> nhẹ CPU
|
| 63 |
+
"-copyts", "-start_at_zero",
|
| 64 |
"-f", "hls",
|
| 65 |
"-hls_time", str(SEG_SEC),
|
| 66 |
"-hls_list_size", str(LIST_SIZE), # giữ ~5 phút
|
| 67 |
+
"-hls_flags", "delete_segments+omit_endlist+independent_segments+program_date_time",
|
| 68 |
+
"-hls_segment_type", "mpegts",
|
| 69 |
"-hls_segment_filename", seg,
|
| 70 |
playlist,
|
| 71 |
]
|
|
|
|
| 104 |
if not os.path.exists(pl):
|
| 105 |
return JSONResponse({"error": "buffer not ready"}, status_code=404)
|
| 106 |
txt = open(pl, "r", encoding="utf-8", errors="ignore").read()
|
| 107 |
+
# rewrite segment names -> absolute URLs for HLS.js
|
| 108 |
out = []
|
| 109 |
for line in txt.split("\n"):
|
| 110 |
s = line.strip()
|
| 111 |
if s and not s.startswith("#") and s.endswith(".ts"):
|
| 112 |
+
# Dùng absolute URL để HLS.js resolve đúng
|
| 113 |
out.append(f"/api/dvr/buffer/seg/{channel_id}/{s}")
|
| 114 |
else:
|
| 115 |
out.append(line)
|
| 116 |
+
# Thêm cache-bust để tránh browser cache cũ
|
| 117 |
return Response("\n".join(out), media_type="application/vnd.apple.mpegurl",
|
| 118 |
+
headers={
|
| 119 |
+
"Access-Control-Allow-Origin": "*",
|
| 120 |
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
| 121 |
+
"Pragma": "no-cache",
|
| 122 |
+
"Expires": "0",
|
| 123 |
+
})
|
| 124 |
|
| 125 |
@router.get("/api/dvr/buffer/seg/{channel_id}/{seg}")
|
| 126 |
def buffer_seg(channel_id: str, seg: str):
|
|
|
|
| 211 |
|
| 212 |
@router.post("/api/dvr/record/schedule")
|
| 213 |
def record_schedule(payload: dict = Body(...)):
|
| 214 |
+
"""body: {channel, start_iso (giờ VN), end_iso (giờ VN), duration_sec, prebuffer?, upload_repo?}"""
|
| 215 |
ch = str(payload.get("channel", "")).lower().strip()
|
| 216 |
if not ch:
|
| 217 |
return JSONResponse({"error": "missing channel"}, status_code=400)
|
| 218 |
+
start_iso = payload.get("start_iso")
|
| 219 |
+
end_iso = payload.get("end_iso")
|
| 220 |
+
if not start_iso:
|
| 221 |
+
return JSONResponse({"error": "missing start_iso"}, status_code=400)
|
| 222 |
+
# Parse và validate timezone
|
| 223 |
+
try:
|
| 224 |
+
start_dt = datetime.fromisoformat(start_iso)
|
| 225 |
+
if start_dt.tzinfo is None:
|
| 226 |
+
start_dt = start_dt.replace(tzinfo=VN_TZ)
|
| 227 |
+
except Exception:
|
| 228 |
+
return JSONResponse({"error": "invalid start_iso"}, status_code=400)
|
| 229 |
+
# Tính duration từ end_iso nếu có, nếu không dùng duration_sec
|
| 230 |
+
if end_iso:
|
| 231 |
+
try:
|
| 232 |
+
end_dt = datetime.fromisoformat(end_iso)
|
| 233 |
+
if end_dt.tzinfo is None:
|
| 234 |
+
end_dt = end_dt.replace(tzinfo=VN_TZ)
|
| 235 |
+
duration_from_end = int((end_dt - start_dt).total_seconds())
|
| 236 |
+
duration = max(5, min(3600, duration_from_end))
|
| 237 |
+
except Exception:
|
| 238 |
+
duration = int(payload.get("duration_sec", 600))
|
| 239 |
+
else:
|
| 240 |
+
duration = int(payload.get("duration_sec", 600))
|
| 241 |
+
duration = max(5, min(3600, duration))
|
| 242 |
job = {
|
| 243 |
"id": str(int(time.time() * 1000)),
|
| 244 |
"channel": ch,
|
| 245 |
+
"start_iso": start_dt.isoformat(),
|
| 246 |
+
"end_iso": end_iso or (start_dt + timedelta(seconds=duration)).isoformat(),
|
| 247 |
+
"duration_sec": duration,
|
| 248 |
"prebuffer": bool(payload.get("prebuffer", True)),
|
| 249 |
"upload_repo": payload.get("upload_repo"),
|
| 250 |
"status": "pending", "file": None,
|