Files
2025-02-18 14:07:52 +00:00

118 lines
4.4 KiB
Python

import asyncio
import io
from collections import deque
from typing import AsyncGenerator, Dict
import aiohttp
from fastapi.responses import StreamingResponse
from utils.config import config
from utils.logger import logger
class StreamManager:
def __init__(
self,
chunk_size: int = config.chunk_size_mb * 1024 * 1024,
buffer_size: int = config.buffer_size_mb * 1024 * 1024,
):
self.chunk_size = chunk_size
self.buffer_size = buffer_size
self.default_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Accept": "*/*",
"Connection": "keep-alive",
}
async def create_streaming_response(
self, url: str, request_headers: Dict
) -> StreamingResponse:
headers = self.default_headers.copy()
if "range" in request_headers:
headers["range"] = request_headers["range"]
timeout = aiohttp.ClientTimeout(total=180)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url, headers=headers) as response:
content_range = response.headers.get("Content-Range", "")
content_length = response.headers.get("Content-Length", "")
response_headers = {
"Accept-Ranges": "bytes",
"Content-Range": content_range if content_range else None,
"Content-Length": content_length if content_length else None,
"Connection": "keep-alive",
"Cache-Control": "no-cache",
}
response_headers = {k: v for k, v in response_headers.items() if v is not None}
media_type = response.headers.get("Content-Type", "video/mp4")
return StreamingResponse(
self._stream_content(url, headers),
media_type=media_type,
status_code=206 if "range" in request_headers else 200,
headers=response_headers,
)
async def _stream_content(
self, url: str, headers: Dict
) -> AsyncGenerator[bytes, None]:
timeout = aiohttp.ClientTimeout(total=None, connect=120, sock_read=120)
buffer = deque()
current_buffer_size = 0
buffer_low_threshold = self.buffer_size * 0.2 # 20% of buffer size
connector = aiohttp.TCPConnector(
limit=0,
ttl_dns_cache=300,
force_close=False,
enable_cleanup_closed=True,
)
async with aiohttp.ClientSession(
timeout=timeout, connector=connector
) as session:
async with session.get(url, headers=headers) as response:
async def fill_buffer():
nonlocal current_buffer_size
while True:
try:
if current_buffer_size < self.buffer_size:
chunk = await response.content.read(self.chunk_size)
if not chunk:
break
buffer.append(chunk)
current_buffer_size += len(chunk)
else:
await asyncio.sleep(0.1)
except (asyncio.TimeoutError, aiohttp.ClientError) as e:
logger.error(f"Buffer filling error: {str(e)}")
await asyncio.sleep(1)
continue
buffer_task = asyncio.create_task(fill_buffer())
try:
while not buffer:
await asyncio.sleep(0.1)
while True:
if not buffer and current_buffer_size == 0:
break
if buffer:
chunk = buffer.popleft()
current_buffer_size -= len(chunk)
yield chunk
if current_buffer_size < buffer_low_threshold:
await asyncio.sleep(0.1)
finally:
buffer_task.cancel()
try:
await buffer_task
except asyncio.CancelledError:
pass