mediaflow-proxy 2.4.3__tar.gz → 2.4.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/PKG-INFO +1 -1
  2. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/configs.py +6 -0
  3. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/handlers.py +5 -3
  4. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/main.py +23 -32
  5. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/mpd_processor.py +87 -23
  6. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/remuxer/media_source.py +3 -1
  7. mediaflow_proxy-2.4.4/mediaflow_proxy/routes/__init__.py +43 -0
  8. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/routes/acestream.py +24 -4
  9. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/routes/proxy.py +24 -7
  10. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/routes/telegram.py +40 -16
  11. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/routes/xtream.py +24 -7
  12. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/base_prebuffer.py +4 -1
  13. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/dash_prebuffer.py +24 -4
  14. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/http_utils.py +4 -0
  15. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/mpd_utils.py +118 -18
  16. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/pyproject.toml +1 -1
  17. mediaflow_proxy-2.4.3/mediaflow_proxy/routes/__init__.py +0 -17
  18. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/.gitignore +0 -0
  19. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/LICENSE +0 -0
  20. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/README.md +0 -0
  21. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/__init__.py +0 -0
  22. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/const.py +0 -0
  23. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/drm/__init__.py +0 -0
  24. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/drm/decrypter.py +0 -0
  25. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/F16Px.py +0 -0
  26. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/__init__.py +0 -0
  27. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/base.py +0 -0
  28. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/dlhd.py +0 -0
  29. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/doodstream.py +0 -0
  30. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/factory.py +0 -0
  31. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/fastream.py +0 -0
  32. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/filelions.py +0 -0
  33. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/filemoon.py +0 -0
  34. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/gupload.py +0 -0
  35. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/livetv.py +0 -0
  36. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/lulustream.py +0 -0
  37. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/maxstream.py +0 -0
  38. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/mixdrop.py +0 -0
  39. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/okru.py +0 -0
  40. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/sportsonline.py +0 -0
  41. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/streamtape.py +0 -0
  42. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/streamwish.py +0 -0
  43. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/supervideo.py +0 -0
  44. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/turbovidplay.py +0 -0
  45. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/uqload.py +0 -0
  46. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/vavoo.py +0 -0
  47. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/vidmoly.py +0 -0
  48. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/vidoza.py +0 -0
  49. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/vixcloud.py +0 -0
  50. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/extractors/voe.py +0 -0
  51. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/middleware.py +0 -0
  52. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/remuxer/__init__.py +0 -0
  53. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/remuxer/audio_transcoder.py +0 -0
  54. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/remuxer/codec_utils.py +0 -0
  55. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/remuxer/container_probe.py +0 -0
  56. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/remuxer/ebml_parser.py +0 -0
  57. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/remuxer/hls_manifest.py +0 -0
  58. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/remuxer/mkv_demuxer.py +0 -0
  59. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/remuxer/mp4_muxer.py +0 -0
  60. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/remuxer/mp4_parser.py +0 -0
  61. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/remuxer/pyav_demuxer.py +0 -0
  62. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/remuxer/transcode_handler.py +0 -0
  63. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/remuxer/transcode_pipeline.py +0 -0
  64. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/remuxer/ts_muxer.py +0 -0
  65. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/remuxer/video_transcoder.py +0 -0
  66. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/routes/extractor.py +0 -0
  67. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/routes/playlist_builder.py +0 -0
  68. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/routes/speedtest.py +0 -0
  69. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/schemas.py +0 -0
  70. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/speedtest/__init__.py +0 -0
  71. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/speedtest/models.py +0 -0
  72. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/speedtest/providers/all_debrid.py +0 -0
  73. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/speedtest/providers/base.py +0 -0
  74. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/speedtest/providers/real_debrid.py +0 -0
  75. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/speedtest/service.py +0 -0
  76. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/static/index.html +0 -0
  77. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/static/logo.png +0 -0
  78. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/static/playlist_builder.html +0 -0
  79. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/static/speedtest.html +0 -0
  80. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/static/speedtest.js +0 -0
  81. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/static/url_generator.html +0 -0
  82. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/__init__.py +0 -0
  83. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/acestream.py +0 -0
  84. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/aes.py +0 -0
  85. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/aesgcm.py +0 -0
  86. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/base64_utils.py +0 -0
  87. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/cache_utils.py +0 -0
  88. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/codec.py +0 -0
  89. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/compat.py +0 -0
  90. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/constanttime.py +0 -0
  91. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/crypto_utils.py +0 -0
  92. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/cryptomath.py +0 -0
  93. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/deprecations.py +0 -0
  94. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/extractor_helpers.py +0 -0
  95. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/hls_prebuffer.py +0 -0
  96. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/hls_utils.py +0 -0
  97. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/http_client.py +0 -0
  98. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/m3u8_processor.py +0 -0
  99. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/packed.py +0 -0
  100. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/python_aes.py +0 -0
  101. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/python_aesgcm.py +0 -0
  102. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/rate_limit_handlers.py +0 -0
  103. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/redis_utils.py +0 -0
  104. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/rijndael.py +0 -0
  105. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/stream_transformers.py +0 -0
  106. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/telegram.py +0 -0
  107. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/tlshashlib.py +0 -0
  108. {mediaflow_proxy-2.4.3 → mediaflow_proxy-2.4.4}/mediaflow_proxy/utils/tlshmac.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mediaflow-proxy
3
- Version: 2.4.3
3
+ Version: 2.4.4
4
4
  Summary: A high-performance proxy server for streaming media, supporting HTTP(S), HLS, and MPEG-DASH with real-time DRM decryption.
5
5
  Project-URL: Homepage, https://github.com/mhdzumair/mediaflow-proxy
6
6
  Project-URL: Repository, https://github.com/mhdzumair/mediaflow-proxy
@@ -64,6 +64,12 @@ class Settings(BaseSettings):
64
64
  dash_prebuffer_emergency_threshold: int = 90 # Emergency threshold percentage to trigger aggressive cache cleanup.
65
65
  dash_prebuffer_inactivity_timeout: int = 60 # Seconds of inactivity before cleaning up stream state.
66
66
  dash_segment_cache_ttl: int = 60 # TTL (seconds) for cached media segments; longer = better for slow playback.
67
+ dash_player_lock_timeout: float = 2.5 # Max wait (seconds) for player requests when a segment lock is busy.
68
+ dash_prebuffer_lock_timeout: float = 0.25 # Max wait (seconds) for background prebuffer lock acquisition.
69
+ dash_prefetch_max_concurrent: int = 1 # Max concurrent live DASH prefetch downloads to reduce lock contention.
70
+ dash_live_initial_media_prebuffer: bool = (
71
+ False # Whether manifest-time prebuffer should fetch live media segments (init segments are still prewarmed).
72
+ )
67
73
  mpd_live_init_cache_ttl: int = 60 # TTL (seconds) for live init segment cache; 0 disables caching.
68
74
  mpd_live_playlist_depth: int = 8 # Number of recent segments to expose per live playlist variant.
69
75
  remux_to_ts: bool = False # Remux fMP4 segments to MPEG-TS for ExoPlayer/VLC compatibility.
@@ -877,10 +877,12 @@ async def get_segment(
877
877
  # - Waiting for existing downloads (via asyncio.Event)
878
878
  # - Starting new download if needed
879
879
  # - Caching the result
880
- # Use a short timeout (1s) for player requests to avoid blocking if prebuffer is busy
881
- # This ensures players get fast responses even when background prefetching is active
880
+ # Player requests should get priority over background prebuffer activity.
881
+ # Use a configurable lock timeout to balance responsiveness and cache reuse.
882
882
  if settings.enable_dash_prebuffer:
883
- segment_content = await dash_prebuffer.get_or_download(segment_url, proxy_headers.request, timeout=1.0)
883
+ segment_content = await dash_prebuffer.get_or_download(
884
+ segment_url, proxy_headers.request, timeout=settings.dash_player_lock_timeout
885
+ )
884
886
  else:
885
887
  # Prebuffer disabled - check cache then download directly
886
888
  segment_content = await get_cached_segment(segment_url)
@@ -12,23 +12,16 @@ from starlette.staticfiles import StaticFiles
12
12
 
13
13
  from mediaflow_proxy.configs import settings
14
14
  from mediaflow_proxy.middleware import UIAccessControlMiddleware
15
- from mediaflow_proxy.routes import (
16
- proxy_router,
17
- extractor_router,
18
- speedtest_router,
19
- playlist_builder_router,
20
- xtream_root_router,
21
- acestream_router,
22
- telegram_router,
23
- )
15
+ from mediaflow_proxy.routes.proxy import proxy_router
16
+ from mediaflow_proxy.routes.extractor import extractor_router
17
+ from mediaflow_proxy.routes.speedtest import speedtest_router
18
+ from mediaflow_proxy.routes.playlist_builder import playlist_builder_router
19
+ from mediaflow_proxy.routes.xtream import xtream_root_router
24
20
  from mediaflow_proxy.schemas import GenerateUrlRequest, GenerateMultiUrlRequest, MultiUrlRequestItem
25
21
  from mediaflow_proxy.utils.crypto_utils import EncryptionHandler, EncryptionMiddleware
26
22
  from mediaflow_proxy.utils import redis_utils
27
23
  from mediaflow_proxy.utils.http_utils import encode_mediaflow_proxy_url
28
24
  from mediaflow_proxy.utils.base64_utils import encode_url_to_base64, decode_base64_url, is_base64_url
29
- from mediaflow_proxy.utils.acestream import acestream_manager
30
- from mediaflow_proxy.remuxer.video_transcoder import get_hw_capability, HWAccelType
31
- from mediaflow_proxy.utils.telegram import telegram_manager
32
25
 
33
26
  logging.basicConfig(level=settings.log_level, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
34
27
  logger = logging.getLogger(__name__)
@@ -61,30 +54,22 @@ async def lifespan(app: FastAPI):
61
54
  # use redis-cli KEYS "mfp:*" | xargs redis-cli DEL
62
55
  logger.info("Cache clearing note: Redis entries will expire via TTL")
63
56
 
64
- # Log transcoding capability
65
- hw = get_hw_capability()
66
- if hw.accel_type != HWAccelType.NONE and settings.transcode_prefer_gpu:
67
- logger.info(
68
- "Transcode ready: GPU %s (encoder=%s) | PyAV pipeline",
69
- hw.accel_type.value,
70
- hw.h264_encoder,
71
- )
72
- else:
73
- logger.info(
74
- "Transcode ready: CPU (%s) | PyAV pipeline",
75
- hw.h264_encoder,
76
- )
77
-
78
57
  yield
79
58
 
80
59
  # Shutdown
81
60
  logger.info("Shutting down...")
82
61
  # Close acestream sessions
83
- await acestream_manager.close()
84
- logger.info("Acestream manager closed")
62
+ if settings.enable_acestream:
63
+ from mediaflow_proxy.utils.acestream import acestream_manager
64
+
65
+ await acestream_manager.close()
66
+ logger.info("Acestream manager closed")
85
67
  # Close telegram session
86
- await telegram_manager.close()
87
- logger.info("Telegram manager closed")
68
+ if settings.enable_telegram:
69
+ from mediaflow_proxy.utils.telegram import telegram_manager
70
+
71
+ await telegram_manager.close()
72
+ logger.info("Telegram manager closed")
88
73
  # Close Redis connections
89
74
  await redis_utils.close_redis()
90
75
  logger.info("Redis connections closed")
@@ -318,8 +303,14 @@ async def check_base64_url(url: str):
318
303
 
319
304
 
320
305
  app.include_router(proxy_router, prefix="/proxy", tags=["proxy"], dependencies=[Depends(verify_api_key)])
321
- app.include_router(acestream_router, prefix="/proxy", tags=["acestream"], dependencies=[Depends(verify_api_key)])
322
- app.include_router(telegram_router, prefix="/proxy", tags=["telegram"], dependencies=[Depends(verify_api_key)])
306
+ if settings.enable_acestream:
307
+ from mediaflow_proxy.routes.acestream import acestream_router
308
+
309
+ app.include_router(acestream_router, prefix="/proxy", tags=["acestream"], dependencies=[Depends(verify_api_key)])
310
+ if settings.enable_telegram:
311
+ from mediaflow_proxy.routes.telegram import telegram_router
312
+
313
+ app.include_router(telegram_router, prefix="/proxy", tags=["telegram"], dependencies=[Depends(verify_api_key)])
323
314
  app.include_router(extractor_router, prefix="/extractor", tags=["extractors"], dependencies=[Depends(verify_api_key)])
324
315
  app.include_router(speedtest_router, prefix="/speedtest", tags=["speedtest"], dependencies=[Depends(verify_api_key)])
325
316
  app.include_router(playlist_builder_router, prefix="/playlist", tags=["playlist"])
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import logging
3
3
  import math
4
+ import statistics
4
5
  import time
5
6
 
6
7
  from fastapi import Request, Response, HTTPException
@@ -30,6 +31,83 @@ def _resolve_ts_mode(request: Request) -> bool:
30
31
  return settings.remux_to_ts
31
32
 
32
33
 
34
+ def _resolve_nominal_duration_mpd_timescale(profile: dict, segments: list[dict]) -> int | None:
35
+ """Resolve a stable nominal segment duration (MPD timescale units) for live sequence math."""
36
+ profile_duration = profile.get("nominal_duration_mpd_timescale")
37
+ if isinstance(profile_duration, (int, float)) and profile_duration > 0:
38
+ return int(profile_duration)
39
+
40
+ durations = []
41
+ for seg in segments:
42
+ seg_duration = seg.get("duration_mpd_timescale")
43
+ if isinstance(seg_duration, (int, float)) and seg_duration > 0:
44
+ durations.append(int(seg_duration))
45
+
46
+ if durations:
47
+ # Use median to avoid jumps when first/last live segments are shorter.
48
+ return int(statistics.median_low(durations))
49
+ return None
50
+
51
+
52
+ def _compute_live_media_sequence(first_segment: dict, profile: dict, segments: list[dict]) -> int:
53
+ """
54
+ Compute a stable HLS media sequence for live playlists.
55
+
56
+ Strategy:
57
+ 1) If MPD explicitly sets @startNumber, trust segment numbering.
58
+ 2) Otherwise derive sequence from timeline time / nominal duration.
59
+ 3) Fall back to segment number or template start number.
60
+ """
61
+ segment_number = first_segment.get("number")
62
+ if profile.get("segment_template_start_number_explicit") and segment_number is not None:
63
+ return max(int(segment_number), 1)
64
+
65
+ timeline_time = first_segment.get("time")
66
+ nominal_duration = _resolve_nominal_duration_mpd_timescale(profile, segments)
67
+ if timeline_time is not None and nominal_duration and nominal_duration > 0:
68
+ return max(math.floor(int(timeline_time) / nominal_duration), 1)
69
+
70
+ if segment_number is not None:
71
+ return max(int(segment_number), 1)
72
+
73
+ template_start = profile.get("segment_template_start_number")
74
+ if isinstance(template_start, int) and template_start > 0:
75
+ return template_start
76
+
77
+ return 1
78
+
79
+
80
+ def _compute_live_playlist_depth(
81
+ is_ts_mode: bool,
82
+ effective_start_offset: float | None,
83
+ extinf_values: list[float],
84
+ ) -> int:
85
+ """
86
+ Compute a resilient live playlist depth to reduce segment expiry skips.
87
+
88
+ We keep a larger floor for fMP4 live (direct mode), and further expand
89
+ depth based on requested start_offset so players have enough headroom
90
+ during transient stalls.
91
+ """
92
+ configured_depth = max(settings.mpd_live_playlist_depth, 1)
93
+ depth_floor = 20 if is_ts_mode else 15
94
+ depth = max(configured_depth, depth_floor)
95
+
96
+ if effective_start_offset is not None and effective_start_offset < 0:
97
+ if extinf_values:
98
+ segment_duration = statistics.median(extinf_values)
99
+ if segment_duration <= 0:
100
+ segment_duration = 4.0
101
+ else:
102
+ segment_duration = 4.0
103
+
104
+ segments_behind_live_edge = math.ceil(abs(effective_start_offset) / segment_duration)
105
+ safety_margin = 10 if is_ts_mode else 12
106
+ depth = max(depth, segments_behind_live_edge + safety_margin)
107
+
108
+ return max(depth, 1)
109
+
110
+
33
111
  async def process_manifest(
34
112
  request: Request,
35
113
  mpd_dict: dict,
@@ -462,8 +540,8 @@ def build_hls_playlist(
462
540
  continue
463
541
 
464
542
  if is_live:
465
- # TS mode uses deeper playlist for ExoPlayer buffering
466
- depth = 20 if is_ts_mode else max(settings.mpd_live_playlist_depth, 1)
543
+ extinf_values_for_depth = [s["extinf"] for s in segments if "extinf" in s]
544
+ depth = _compute_live_playlist_depth(is_ts_mode, effective_start_offset, extinf_values_for_depth)
467
545
  trimmed_segments = segments[-depth:]
468
546
  else:
469
547
  trimmed_segments = segments
@@ -479,31 +557,13 @@ def build_hls_playlist(
479
557
  else:
480
558
  target_duration = math.ceil(max(extinf_values)) if extinf_values else 3
481
559
 
482
- # Align HLS media sequence with MPD-provided numbering when available
483
- if is_ts_mode and is_live:
484
- # For live TS, derive sequence from timeline first for stable continuity
485
- time_val = first_segment.get("time")
486
- duration_val = first_segment.get("duration_mpd_timescale")
487
- if time_val is not None and duration_val and duration_val > 0:
488
- sequence = math.floor(time_val / duration_val)
489
- else:
490
- sequence = first_segment.get("number") or profile.get("segment_template_start_number") or 1
560
+ if is_live:
561
+ sequence = _compute_live_media_sequence(first_segment, profile, trimmed_segments)
491
562
  else:
492
563
  mpd_start_number = profile.get("segment_template_start_number")
493
564
  sequence = first_segment.get("number")
494
-
495
565
  if sequence is None:
496
- # Fallback to MPD template start number
497
- if mpd_start_number is not None:
498
- sequence = mpd_start_number
499
- else:
500
- # As a last resort, derive from timeline information
501
- time_val = first_segment.get("time")
502
- duration_val = first_segment.get("duration_mpd_timescale")
503
- if time_val is not None and duration_val and duration_val > 0:
504
- sequence = math.floor(time_val / duration_val)
505
- else:
506
- sequence = 1
566
+ sequence = mpd_start_number if mpd_start_number is not None else 1
507
567
 
508
568
  hls.extend(
509
569
  [
@@ -543,6 +603,10 @@ def build_hls_playlist(
543
603
  if query_params.get("api_password"):
544
604
  init_query_params["api_password"] = query_params["api_password"]
545
605
 
606
+ for k, v in query_params.items():
607
+ if k.startswith("rp_") or k.startswith("h_"):
608
+ init_query_params[k] = v
609
+
546
610
  init_map_url = encode_mediaflow_proxy_url(
547
611
  init_proxy_url,
548
612
  query_params=init_query_params,
@@ -13,7 +13,6 @@ from typing import Protocol, runtime_checkable
13
13
  from urllib.parse import urlparse, unquote
14
14
 
15
15
  from mediaflow_proxy.utils.http_client import create_aiohttp_session
16
- from mediaflow_proxy.utils.telegram import telegram_manager
17
16
 
18
17
  logger = logging.getLogger(__name__)
19
18
 
@@ -142,6 +141,9 @@ class TelegramMediaSource:
142
141
  return self._filename_hint
143
142
 
144
143
  async def stream(self, offset: int = 0, limit: int | None = None) -> AsyncIterator[bytes]:
144
+ # Lazy import to avoid loading Telegram dependencies for non-Telegram routes.
145
+ from mediaflow_proxy.utils.telegram import telegram_manager
146
+
145
147
  effective_limit = limit or self._file_size
146
148
  if self._use_single_client:
147
149
  async for chunk in telegram_manager.stream_media_single(
@@ -0,0 +1,43 @@
1
+ __all__ = [
2
+ "proxy_router",
3
+ "extractor_router",
4
+ "speedtest_router",
5
+ "playlist_builder_router",
6
+ "xtream_root_router",
7
+ "acestream_router",
8
+ "telegram_router",
9
+ ]
10
+
11
+
12
+ def __getattr__(name: str):
13
+ # Lazy import routers so importing a single route module does not
14
+ # pull in optional integrations (telegram/acestream/transcode) at startup.
15
+ if name == "proxy_router":
16
+ from .proxy import proxy_router
17
+
18
+ return proxy_router
19
+ if name == "extractor_router":
20
+ from .extractor import extractor_router
21
+
22
+ return extractor_router
23
+ if name == "speedtest_router":
24
+ from .speedtest import speedtest_router
25
+
26
+ return speedtest_router
27
+ if name == "playlist_builder_router":
28
+ from .playlist_builder import playlist_builder_router
29
+
30
+ return playlist_builder_router
31
+ if name == "xtream_root_router":
32
+ from .xtream import xtream_root_router
33
+
34
+ return xtream_root_router
35
+ if name == "acestream_router":
36
+ from .acestream import acestream_router
37
+
38
+ return acestream_router
39
+ if name == "telegram_router":
40
+ from .telegram import telegram_router
41
+
42
+ return telegram_router
43
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -9,7 +9,8 @@ Provides endpoints for proxying acestream content:
9
9
 
10
10
  import asyncio
11
11
  import logging
12
- from typing import Annotated
12
+ from functools import lru_cache
13
+ from typing import Annotated, TYPE_CHECKING
13
14
  from urllib.parse import urlencode, urljoin, urlparse
14
15
 
15
16
  import aiohttp
@@ -17,8 +18,6 @@ from fastapi import APIRouter, Query, Request, HTTPException, Response, Depends
17
18
  from starlette.background import BackgroundTask
18
19
 
19
20
  from mediaflow_proxy.configs import settings
20
- from mediaflow_proxy.remuxer.transcode_pipeline import stream_transcode_universal
21
- from mediaflow_proxy.utils.acestream import acestream_manager, AcestreamSession
22
21
  from mediaflow_proxy.utils.http_client import create_aiohttp_session
23
22
  from mediaflow_proxy.utils.http_utils import (
24
23
  get_original_scheme,
@@ -34,6 +33,22 @@ from mediaflow_proxy.utils.hls_prebuffer import hls_prebuffer
34
33
  logger = logging.getLogger(__name__)
35
34
  acestream_router = APIRouter()
36
35
 
36
+ if TYPE_CHECKING:
37
+ from mediaflow_proxy.utils.acestream import AcestreamSession
38
+
39
+
40
+ def _get_acestream_manager():
41
+ from mediaflow_proxy.utils.acestream import acestream_manager
42
+
43
+ return acestream_manager
44
+
45
+
46
+ @lru_cache(maxsize=1)
47
+ def _load_transcode_pipeline():
48
+ from mediaflow_proxy.remuxer.transcode_pipeline import stream_transcode_universal
49
+
50
+ return stream_transcode_universal
51
+
37
52
 
38
53
  class AcestreamM3U8Processor(M3U8Processor):
39
54
  """
@@ -46,7 +61,7 @@ class AcestreamM3U8Processor(M3U8Processor):
46
61
  def __init__(
47
62
  self,
48
63
  request: Request,
49
- session: AcestreamSession,
64
+ session: "AcestreamSession",
50
65
  key_url: str = None,
51
66
  force_playlist_proxy: bool = True,
52
67
  key_only_proxy: bool = False,
@@ -140,6 +155,7 @@ async def acestream_hls_manifest(
140
155
  """
141
156
  if not settings.enable_acestream:
142
157
  raise HTTPException(status_code=503, detail="Acestream support is disabled")
158
+ acestream_manager = _get_acestream_manager()
143
159
 
144
160
  if not infohash and not id:
145
161
  raise HTTPException(status_code=400, detail="Either 'infohash' or 'id' parameter is required")
@@ -278,6 +294,7 @@ async def acestream_segment_proxy(
278
294
  """
279
295
  if not settings.enable_acestream:
280
296
  raise HTTPException(status_code=503, detail="Acestream support is disabled")
297
+ acestream_manager = _get_acestream_manager()
281
298
 
282
299
  # Use id or infohash for session lookup
283
300
  session_key = id or infohash
@@ -368,6 +385,7 @@ async def acestream_ts_stream(
368
385
  """
369
386
  if not settings.enable_acestream:
370
387
  raise HTTPException(status_code=503, detail="Acestream support is disabled")
388
+ acestream_manager = _get_acestream_manager()
371
389
 
372
390
  if not infohash and not id:
373
391
  raise HTTPException(status_code=400, detail="Either 'infohash' or 'id' parameter is required")
@@ -438,6 +456,7 @@ async def acestream_ts_stream(
438
456
  # Use our custom PyAV pipeline with forced video re-encoding
439
457
  # (live MPEG-TS sources often have corrupt H.264 bitstreams
440
458
  # that browsers reject; re-encoding produces a clean stream).
459
+ stream_transcode_universal = _load_transcode_pipeline()
441
460
  content = stream_transcode_universal(
442
461
  _acestream_ts_source(),
443
462
  force_video_reencode=True,
@@ -509,6 +528,7 @@ async def acestream_status(
509
528
  """
510
529
  if not settings.enable_acestream:
511
530
  raise HTTPException(status_code=503, detail="Acestream support is disabled")
531
+ acestream_manager = _get_acestream_manager()
512
532
 
513
533
  if infohash:
514
534
  session = acestream_manager.get_session(infohash)
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import logging
3
3
  import re
4
+ from functools import lru_cache
4
5
  from typing import Annotated
5
6
  from urllib.parse import quote, unquote
6
7
 
@@ -41,19 +42,31 @@ from mediaflow_proxy.utils.http_utils import (
41
42
  from mediaflow_proxy.utils.http_client import create_aiohttp_session
42
43
  from mediaflow_proxy.utils.m3u8_processor import M3U8Processor
43
44
  from mediaflow_proxy.utils.stream_transformers import apply_transformer_to_bytes
44
- from mediaflow_proxy.remuxer.media_source import HTTPMediaSource
45
- from mediaflow_proxy.remuxer.transcode_handler import (
46
- handle_transcode,
47
- handle_transcode_hls_init,
48
- handle_transcode_hls_playlist,
49
- handle_transcode_hls_segment,
50
- )
51
45
 
52
46
 
53
47
  logger = logging.getLogger(__name__)
54
48
  proxy_router = APIRouter()
55
49
 
56
50
 
51
+ @lru_cache(maxsize=1)
52
+ def _load_transcode_components():
53
+ from mediaflow_proxy.remuxer.media_source import HTTPMediaSource
54
+ from mediaflow_proxy.remuxer.transcode_handler import (
55
+ handle_transcode,
56
+ handle_transcode_hls_init,
57
+ handle_transcode_hls_playlist,
58
+ handle_transcode_hls_segment,
59
+ )
60
+
61
+ return (
62
+ HTTPMediaSource,
63
+ handle_transcode,
64
+ handle_transcode_hls_init,
65
+ handle_transcode_hls_playlist,
66
+ handle_transcode_hls_segment,
67
+ )
68
+
69
+
57
70
  def sanitize_url(url: str) -> str:
58
71
  """
59
72
  Sanitize URL to fix common encoding issues and handle base64 encoded URLs.
@@ -499,6 +512,7 @@ async def transcode_hls_playlist(
499
512
  """
500
513
  if not settings.enable_transcode:
501
514
  raise HTTPException(status_code=503, detail="Transcoding support is disabled")
515
+ HTTPMediaSource, _, _, handle_transcode_hls_playlist, _ = _load_transcode_components()
502
516
  destination = sanitize_url(destination)
503
517
  source = HTTPMediaSource(url=destination, headers=dict(proxy_headers.request))
504
518
  await source.resolve_file_size()
@@ -540,6 +554,7 @@ async def transcode_hls_init(
540
554
  """
541
555
  if not settings.enable_transcode:
542
556
  raise HTTPException(status_code=503, detail="Transcoding support is disabled")
557
+ HTTPMediaSource, _, handle_transcode_hls_init, _, _ = _load_transcode_components()
543
558
  destination = sanitize_url(destination)
544
559
  source = HTTPMediaSource(url=destination, headers=dict(proxy_headers.request))
545
560
  await source.resolve_file_size()
@@ -572,6 +587,7 @@ async def transcode_hls_segment(
572
587
  """
573
588
  if not settings.enable_transcode:
574
589
  raise HTTPException(status_code=503, detail="Transcoding support is disabled")
590
+ HTTPMediaSource, _, _, _, handle_transcode_hls_segment = _load_transcode_components()
575
591
  destination = sanitize_url(destination)
576
592
  source = HTTPMediaSource(url=destination, headers=dict(proxy_headers.request))
577
593
  await source.resolve_file_size()
@@ -693,6 +709,7 @@ async def proxy_stream_endpoint(
693
709
  if transcode:
694
710
  if not settings.enable_transcode:
695
711
  raise HTTPException(status_code=503, detail="Transcoding support is disabled")
712
+ HTTPMediaSource, handle_transcode, _, _, _ = _load_transcode_components()
696
713
  transcode_headers = dict(proxy_headers.request)
697
714
  transcode_headers.pop("range", None)
698
715
  transcode_headers.pop("if-range", None)
@@ -12,37 +12,50 @@ import asyncio
12
12
  import logging
13
13
  import re
14
14
  import secrets
15
- from typing import Annotated, Optional
15
+ from functools import lru_cache
16
+ from typing import Annotated, Optional, TYPE_CHECKING
16
17
 
17
18
  from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
18
19
  from pydantic import BaseModel
19
20
 
20
- from telethon import TelegramClient
21
- from telethon.sessions import StringSession
22
-
23
21
  from mediaflow_proxy.configs import settings
24
22
  from mediaflow_proxy.remuxer.media_source import TelegramMediaSource
25
- from mediaflow_proxy.remuxer.transcode_handler import (
26
- handle_transcode,
27
- handle_transcode_hls_init,
28
- handle_transcode_hls_playlist,
29
- handle_transcode_hls_segment,
30
- )
31
23
  from mediaflow_proxy.utils.http_utils import (
32
24
  EnhancedStreamingResponse,
33
25
  ProxyRequestHeaders,
34
26
  apply_header_manipulation,
35
27
  get_proxy_headers,
36
28
  )
37
- from mediaflow_proxy.utils.telegram import (
38
- TelegramMediaRef,
39
- parse_telegram_url,
40
- telegram_manager,
41
- )
42
29
 
43
30
  logger = logging.getLogger(__name__)
44
31
  telegram_router = APIRouter()
45
32
 
33
+ if TYPE_CHECKING:
34
+ from mediaflow_proxy.utils.telegram import TelegramMediaRef
35
+
36
+
37
+ def _telegram_utils():
38
+ from mediaflow_proxy.utils.telegram import TelegramMediaRef, parse_telegram_url, telegram_manager
39
+
40
+ return TelegramMediaRef, parse_telegram_url, telegram_manager
41
+
42
+
43
+ @lru_cache(maxsize=1)
44
+ def _load_transcode_handlers():
45
+ from mediaflow_proxy.remuxer.transcode_handler import (
46
+ handle_transcode,
47
+ handle_transcode_hls_init,
48
+ handle_transcode_hls_playlist,
49
+ handle_transcode_hls_segment,
50
+ )
51
+
52
+ return (
53
+ handle_transcode,
54
+ handle_transcode_hls_init,
55
+ handle_transcode_hls_playlist,
56
+ handle_transcode_hls_segment,
57
+ )
58
+
46
59
 
47
60
  def get_content_type(mime_type: str, file_name: Optional[str] = None) -> str:
48
61
  """Determine content type from mime type or filename."""
@@ -165,6 +178,7 @@ async def telegram_stream(
165
178
  """
166
179
  if not settings.enable_telegram:
167
180
  raise HTTPException(status_code=503, detail="Telegram proxy support is disabled")
181
+ TelegramMediaRef, parse_telegram_url, telegram_manager = _telegram_utils()
168
182
 
169
183
  # Get the URL from either parameter
170
184
  telegram_url = d or url
@@ -359,7 +373,7 @@ async def telegram_stream(
359
373
 
360
374
  async def _handle_transcode(
361
375
  request: Request,
362
- ref: TelegramMediaRef,
376
+ ref: "TelegramMediaRef",
363
377
  file_size: int,
364
378
  start_time: float | None = None,
365
379
  file_name: str = "",
@@ -371,6 +385,7 @@ async def _handle_transcode(
371
385
  passes it to the source-agnostic transcode handler which handles
372
386
  cue probing, seeking, and pipeline selection.
373
387
  """
388
+ handle_transcode, _, _, _ = _load_transcode_handlers()
374
389
  source = TelegramMediaSource(ref, file_size, file_name=file_name)
375
390
  return await handle_transcode(request, source, start_time=start_time)
376
391
 
@@ -406,6 +421,7 @@ async def _resolve_telegram_source(
406
421
  from fastapi import HTTPException
407
422
 
408
423
  raise HTTPException(status_code=503, detail="Telegram proxy support is disabled")
424
+ TelegramMediaRef, parse_telegram_url, telegram_manager = _telegram_utils()
409
425
 
410
426
  telegram_url = d or url
411
427
 
@@ -462,6 +478,7 @@ async def telegram_transcode_hls_playlist(
462
478
  """Generate an HLS VOD M3U8 playlist for a Telegram media file."""
463
479
  if not settings.enable_transcode:
464
480
  raise HTTPException(status_code=503, detail="Transcoding support is disabled")
481
+ _, _, handle_transcode_hls_playlist, _ = _load_transcode_handlers()
465
482
  source = await _resolve_telegram_source(
466
483
  d,
467
484
  url,
@@ -504,6 +521,7 @@ async def telegram_transcode_hls_init(
504
521
  """Serve the fMP4 init segment for a Telegram media file."""
505
522
  if not settings.enable_transcode:
506
523
  raise HTTPException(status_code=503, detail="Transcoding support is disabled")
524
+ _, handle_transcode_hls_init, _, _ = _load_transcode_handlers()
507
525
  source = await _resolve_telegram_source(
508
526
  d,
509
527
  url,
@@ -534,6 +552,7 @@ async def telegram_transcode_hls_segment(
534
552
  """Serve a single HLS fMP4 media segment for a Telegram media file."""
535
553
  if not settings.enable_transcode:
536
554
  raise HTTPException(status_code=503, detail="Transcoding support is disabled")
555
+ _, _, _, handle_transcode_hls_segment = _load_transcode_handlers()
537
556
  source = await _resolve_telegram_source(
538
557
  d,
539
558
  url,
@@ -628,6 +647,7 @@ async def telegram_info(
628
647
  """
629
648
  if not settings.enable_telegram:
630
649
  raise HTTPException(status_code=503, detail="Telegram proxy support is disabled")
650
+ TelegramMediaRef, parse_telegram_url, telegram_manager = _telegram_utils()
631
651
 
632
652
  telegram_url = d or url
633
653
 
@@ -718,6 +738,7 @@ async def telegram_status():
718
738
  }
719
739
 
720
740
  # Check if client is connected
741
+ _, _, telegram_manager = _telegram_utils()
721
742
  if telegram_manager.is_initialized:
722
743
  return {
723
744
  "enabled": True,
@@ -783,6 +804,9 @@ async def session_start(request: SessionStartRequest):
783
804
  session_id = secrets.token_urlsafe(16)
784
805
 
785
806
  try:
807
+ from telethon import TelegramClient
808
+ from telethon.sessions import StringSession
809
+
786
810
  client = TelegramClient(StringSession(), request.api_id, request.api_hash)
787
811
  await client.connect()
788
812