bep40 commited on
Commit
9a5984e
·
verified ·
1 Parent(s): b29b73e

fix: main.py - persistent pump + async refresh + hls live opts

Browse files
Files changed (1) hide show
  1. main.py +193 -58
main.py CHANGED
@@ -214,38 +214,32 @@ def proxy_m3u_hls(url: str = Query(...), ua: str = Query(default=None), cookie:
214
  )
215
  except Exception:
216
  return Response(status_code=502, content="proxy error")
217
- # Raw MPEG-TS / Stalker stream: emit a live HLS wrapper playlist.
218
- # FIX: Emit a LIVE sliding window with multiple #EXTINF segments (all
219
- # pointing at the same /api/proxy/seg URL). The old single-segment manifest
220
- # caused Hls.js to play ~8s of data and then stall — the playlist had no
221
- # new segments to discover. With 3 segments Hls.js keeps the playback
222
- # buffer alive and polls for the next window, giving continuous live
223
- # playback for MPEG-TS / Stalker streams.
224
- _seg = seg_proxy_base
225
- manifest = (
226
- "#EXTM3U\n"
227
- "#EXT-X-VERSION:6\n"
228
- "#EXT-X-TARGETDURATION:8\n"
229
- "#EXT-X-MEDIA-SEQUENCE:0\n"
230
- "#EXT-X-DISCONTINUITY-SEQUENCE:0\n"
231
- "#EXT-X-START:TIME-OFFSET=0,PRECISE=YES\n"
232
- "#EXTINF:8.0,\n"
233
- + _seg + "\n"
234
- "#EXTINF:8.0,\n"
235
- + _seg + "\n"
236
- "#EXTINF:8.0,\n"
237
- + _seg + "\n"
238
- "#EXTINF:8.0,\n"
239
- + _seg + "\n"
240
- "#EXTINF:8.0,\n"
241
- + _seg + "\n"
242
- "#EXTINF:8.0,\n"
243
- + _seg + "\n"
244
- "#EXTINF:8.0,\n"
245
- + _seg + "\n"
246
- "#EXTINF:8.0,\n"
247
- + _seg + "\n"
248
- )
249
  return Response(content=manifest.encode("utf-8"),
250
  media_type="application/vnd.apple.mpegurl",
251
  headers={"Access-Control-Allow-Origin":"*","Cache-Control":"no-cache, no-store, must-revalidate"})
@@ -311,25 +305,172 @@ def proxy_stream(url: str = Query(...), ua: str = Query(default=None), cookie: s
311
  except Exception:
312
  return Response(status_code=502, content="proxy error")
313
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  @app.get("/api/proxy/seg")
315
- def proxy_segment(url: str = Query(...), ua: str = Query(default=None), cookie: str = Query(default=None), auth: str = Query(default=None), fallbacks: str = Query(default=None), max_sec: float = Query(default=0)):
316
- """Proxy a stream segment. For live MPEG-TS streams, streams the raw data
317
- continuously so Hls.js can buffer and play it. If the fetched URL turns
318
- out to be an HLS *playlist* (master or media), it is rewritten so the
319
- downstream segment(s) also go through this proxy. The max_sec parameter
320
- is accepted for backwards compatibility but the stream is left open-ended
321
- (the client / CDN closes the read when sufficient data is buffered or the
322
- user navigates away)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  try:
324
- seg_headers = dict(HEADERS)
325
- if ua: seg_headers["User-Agent"] = ua
326
- if cookie: seg_headers["Cookie"] = cookie
327
- if auth: seg_headers["Authorization"] = auth
328
- # FIX: try the primary URL first; if it returns non-200 (e.g. an
329
- # expired stalker play_token -> 502/403), fall through the "fallbacks"
330
- # list (same stream ID on alternate servers) and use the first that
331
- # returns 200. This fixes "black screen" on Stalker channels whose
332
- # cached token on one server expired but works on another.
333
  import json as _json
334
  fb_urls = []
335
  if fallbacks:
@@ -343,11 +484,6 @@ def proxy_segment(url: str = Query(...), ua: str = Query(default=None), cookie:
343
  try:
344
  r = requests.get(attempt_url, headers=seg_headers, timeout=(8, 300), stream=True)
345
  if r.status_code == 200:
346
- # Sniff the first bytes: if it's an HLS playlist (e.g. a
347
- # master playlist from "/api/proxy/m3u_hls" for an .m3u8
348
- # URL, or a media playlist variant), rewrite it so every
349
- # segment goes through this proxy again. MPEG-TS (0x47
350
- # sync bytes) or other media streams pass through raw.
351
  lead = b""
352
  try:
353
  lead = next(r.iter_content(4096))
@@ -362,16 +498,13 @@ def proxy_segment(url: str = Query(...), ua: str = Query(default=None), cookie:
362
  body = rr.text
363
  except Exception:
364
  pass
365
- # Resolve against the playlist location (some variants
366
- # use relative URIs) — use the attempt URL as base.
367
  rewritten, _ = rewrite_hls_playlist(body, attempt_url, ua, cookie, auth, None)
368
  return Response(
369
  content=rewritten.encode("utf-8"),
370
  media_type="application/vnd.apple.mpegurl",
371
  headers={"Access-Control-Allow-Origin":"*", "Cache-Control":"no-cache, no-store, must-revalidate"},
372
  )
373
- # Real media segment — stream through.
374
- def _stream(src=r):
375
  if lead:
376
  yield lead
377
  for chunk in src.iter_content(chunk_size=65536):
@@ -390,6 +523,8 @@ def proxy_segment(url: str = Query(...), ua: str = Query(default=None), cookie:
390
  except Exception:
391
  return Response(status_code=502, content="proxy error")
392
 
 
 
393
  @app.get("/api/proxy/video")
394
  def proxy_video(url: str = Query(...), request: Request = None):
395
  try:
 
214
  )
215
  except Exception:
216
  return Response(status_code=502, content="proxy error")
217
+ # Raw MPEG-TS / Stalker / unknown: serve a SLIDING WINDOW from
218
+ # ONE persistent upstream pump. The old manifest listed 8
219
+ # identical /api/proxy/seg URLs -> hls.js treated segments as
220
+ # replayed after ~64s (buffer drains -> giât) and every segment
221
+ # open a NEW upstream connection (~3.4s reconnect stalls).
222
+ from urllib.parse import urlencode as _ue
223
+ _key = _hls_pump_key(url, ua or "", cookie or "", auth or "")
224
+ _live = _hls_pump_progress(_key, url, headers)
225
+ _seq = max(0, (_live // _HLS_WINDOW) - _HLS_SEGS)
226
+ _man_lines = [
227
+ "#EXTM3U",
228
+ "#EXT-X-VERSION:6",
229
+ "#EXT-X-TARGETDURATION:10",
230
+ "#EXT-X-MEDIA-SEQUENCE:" + str(_seq),
231
+ "#EXT-X-DISCONTINUITY-SEQUENCE:0",
232
+ "#EXT-X-START:TIME-OFFSET=0,PRECISE=YES",
233
+ ]
234
+ for _i in range(_HLS_SEGS):
235
+ _q = _ue({"url": url})
236
+ if ua: _q += "&ua=" + quote(ua, safe="")
237
+ if cookie: _q += "&cookie=" + quote(cookie, safe="")
238
+ if auth: _q += "&auth=" + quote(auth, safe="")
239
+ _q += "&off=" + str((_seq + _i) * _HLS_WINDOW)
240
+ _man_lines.append("#EXTINF:8.0,")
241
+ _man_lines.append("/api/proxy/seg?" + _q)
242
+ manifest = "\n".join(_man_lines) + "\n"
 
 
 
 
 
 
243
  return Response(content=manifest.encode("utf-8"),
244
  media_type="application/vnd.apple.mpegurl",
245
  headers={"Access-Control-Allow-Origin":"*","Cache-Control":"no-cache, no-store, must-revalidate"})
 
305
  except Exception:
306
  return Response(status_code=502, content="proxy error")
307
 
308
+ _HLS_PUMPS = {}
309
+ _HLS_PUMPS_LOCK = threading.Lock()
310
+ _HLS_WINDOW = 4194280
311
+ _HLS_SEGS = 4
312
+ _HLS_KEEP = 8 * _HLS_WINDOW
313
+
314
+
315
+ def _hls_pump_key(url, ua, cookie, auth):
316
+ return (url or "") + "\x1f" + (ua or "") + "\x1f" + (cookie or "") + "\x1f" + (auth or "")
317
+
318
+
319
+ class _UpstreamPump:
320
+ """Hold ONE connection to a raw MPEG-TS / mp2t / .ts stream and buffer it
321
+ as a monotonic byte stream. Segment requests stream byte ranges from this
322
+ buffer, so N consecutive HLS segments share ONE upstream connection —
323
+ eliminating the per-segment reconnect that caused the giât on restreamed
324
+ .ts/.mp2t channels."""
325
+
326
+ def __init__(self, url, headers):
327
+ self.url = url
328
+ self.headers = headers
329
+ self.buf = bytearray()
330
+ self.base = 0
331
+ self.live = 0
332
+ self.dead = False
333
+ self.err = ""
334
+ self.last_use = time.time()
335
+ self.cv = threading.Condition()
336
+ self._t = threading.Thread(target=self._run, daemon=True)
337
+
338
+ def start(self):
339
+ self._t.start()
340
+
341
+ def _run(self):
342
+ rr = None
343
+ try:
344
+ rr = requests.get(self.url, headers=self.headers, timeout=(10, 120),
345
+ stream=True, allow_redirects=True)
346
+ if rr.status_code != 200:
347
+ with self.cv:
348
+ self.err = "upstream %s" % rr.status_code
349
+ self.dead = True
350
+ self.cv.notify_all()
351
+ return
352
+ for chunk in rr.iter_content(chunk_size=65536):
353
+ if not chunk:
354
+ continue
355
+ with self.cv:
356
+ self.buf.extend(chunk)
357
+ self.live += len(chunk)
358
+ if len(self.buf) > _HLS_KEEP:
359
+ drop = len(self.buf) - _HLS_KEEP
360
+ del self.buf[:drop]
361
+ self.base += drop
362
+ self.cv.notify_all()
363
+ except Exception as e:
364
+ self.err = str(e)[:120]
365
+ finally:
366
+ if rr is not None:
367
+ try:
368
+ rr.close()
369
+ except Exception:
370
+ pass
371
+ with self.cv:
372
+ self.dead = True
373
+ self.cv.notify_all()
374
+
375
+ def stream(self, off, want):
376
+ """Yield bytes from absolute offset `off`, up to `want` bytes total.
377
+ Blocks waiting for data; ends at EOF/error or after `want` bytes."""
378
+ self.last_use = time.time()
379
+ sent = 0
380
+ while sent < want:
381
+ with self.cv:
382
+ if off < self.base:
383
+ off = self.base
384
+ have = self.live - off
385
+ if have <= 0:
386
+ if self.dead:
387
+ return
388
+ self.cv.wait(0.5)
389
+ continue
390
+ n = min(want - sent, have)
391
+ start = off - self.base
392
+ data = bytes(self.buf[start:start + n])
393
+ off += n
394
+ if not data:
395
+ return
396
+ sent += len(data)
397
+ yield data
398
+
399
+
400
+ def _hls_get_pump(key, url, headers):
401
+ with _HLS_PUMPS_LOCK:
402
+ p = _HLS_PUMPS.get(key)
403
+ if p is None or p.dead:
404
+ p = _UpstreamPump(url, headers)
405
+ p.start()
406
+ _HLS_PUMPS[key] = p
407
+ return p
408
+
409
+
410
+ def _hls_pump_progress(key, url, headers):
411
+ with _HLS_PUMPS_LOCK:
412
+ p = _HLS_PUMPS.get(key)
413
+ if p is None:
414
+ p = _hls_get_pump(key, url, headers)
415
+ return getattr(p, "live", 0)
416
+
417
+
418
+ def _hls_pump_janitor():
419
+ while True:
420
+ time.sleep(300)
421
+ try:
422
+ now = time.time()
423
+ with _HLS_PUMPS_LOCK:
424
+ for k in list(_HLS_PUMPS):
425
+ p = _HLS_PUMPS[k]
426
+ if p.dead or now - p.last_use > 900:
427
+ try:
428
+ p.buf = bytearray()
429
+ except Exception:
430
+ pass
431
+ del _HLS_PUMPS[k]
432
+ except Exception:
433
+ pass
434
+
435
+
436
+ try:
437
+ threading.Thread(target=_hls_pump_janitor, daemon=True).start()
438
+ except Exception:
439
+ pass
440
+
441
+
442
  @app.get("/api/proxy/seg")
443
+ def proxy_segment(url: str = Query(...), ua: str = Query(default=None), cookie: str = Query(default=None), auth: str = Query(default=None), fallbacks: str = Query(default=None), max_sec: float = Query(default=0), off: int = Query(default=-1)):
444
+ """Proxy a stream segment.
445
+
446
+ - off >= 0: raw-stream path served from the shared pump (ONE persistent
447
+ upstream connection). Consecutive HLS windows read continuous byte ranges
448
+ from the same connection -> no reconnect giât on restreamed .ts/.mp2t.
449
+ - off < 0: legacy path. Probes the upstream; HLS playlists (master/media)
450
+ are rewritten so their segments also go through this proxy.
451
+ """
452
+ seg_headers = dict(HEADERS)
453
+ if ua: seg_headers["User-Agent"] = ua
454
+ if cookie: seg_headers["Cookie"] = cookie
455
+ if auth: seg_headers["Authorization"] = auth
456
+
457
+ if off >= 0:
458
+ key = _hls_pump_key(url, ua, cookie, auth)
459
+ p = _hls_get_pump(key, url, seg_headers)
460
+
461
+ def _gen():
462
+ try:
463
+ yield from p.stream(off, _HLS_WINDOW)
464
+ except Exception:
465
+ return
466
+ return StreamingResponse(
467
+ _gen(),
468
+ media_type="video/mp2t",
469
+ headers={"Access-Control-Allow-Origin": "*", "Cache-Control": "no-cache, no-store"},
470
+ )
471
+
472
+ # ---------- legacy sniff path (HLS playlists / fallbacks) ----------
473
  try:
 
 
 
 
 
 
 
 
 
474
  import json as _json
475
  fb_urls = []
476
  if fallbacks:
 
484
  try:
485
  r = requests.get(attempt_url, headers=seg_headers, timeout=(8, 300), stream=True)
486
  if r.status_code == 200:
 
 
 
 
 
487
  lead = b""
488
  try:
489
  lead = next(r.iter_content(4096))
 
498
  body = rr.text
499
  except Exception:
500
  pass
 
 
501
  rewritten, _ = rewrite_hls_playlist(body, attempt_url, ua, cookie, auth, None)
502
  return Response(
503
  content=rewritten.encode("utf-8"),
504
  media_type="application/vnd.apple.mpegurl",
505
  headers={"Access-Control-Allow-Origin":"*", "Cache-Control":"no-cache, no-store, must-revalidate"},
506
  )
507
+ def _stream(src=r, lead=lead):
 
508
  if lead:
509
  yield lead
510
  for chunk in src.iter_content(chunk_size=65536):
 
523
  except Exception:
524
  return Response(status_code=502, content="proxy error")
525
 
526
+
527
+
528
  @app.get("/api/proxy/video")
529
  def proxy_video(url: str = Query(...), request: Request = None):
530
  try: