improved video play
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from flask import Flask, request, Response, send_from_directory, jsonify
|
||||
import os
|
||||
import requests
|
||||
from flask_cors import CORS
|
||||
import urllib.parse
|
||||
@@ -114,6 +115,14 @@ def favicon():
|
||||
def stream_video():
|
||||
# Note: <video> tags perform GET. To support your POST requirement,
|
||||
# we handle the URL via JSON post or URL params.
|
||||
debug_param = os.getenv('STREAM_DEBUG', '').strip().lower()
|
||||
debug_enabled = debug_param in ('1', 'true', 'yes', 'on')
|
||||
cookie_param = os.getenv('STREAM_FORWARD_COOKIES', '').strip().lower()
|
||||
forward_cookies = cookie_param in ('1', 'true', 'yes', 'on')
|
||||
def dbg(message):
|
||||
if debug_enabled:
|
||||
app.logger.info("[stream_video] %s", message)
|
||||
|
||||
video_url = ""
|
||||
if request.method == 'POST':
|
||||
video_url = request.json.get('url')
|
||||
@@ -123,6 +132,8 @@ def stream_video():
|
||||
if not video_url:
|
||||
return jsonify({"error": "No URL provided"}), 400
|
||||
|
||||
dbg(f"method={request.method} url={video_url}")
|
||||
|
||||
def is_hls(url):
|
||||
return '.m3u8' in urllib.parse.urlparse(url).path
|
||||
|
||||
@@ -155,26 +166,52 @@ def stream_video():
|
||||
path = urllib.parse.urlparse(url).path.lower()
|
||||
return any(path.endswith(ext) for ext in ('.mp4', '.m4v', '.m4s', '.ts', '.webm', '.mov'))
|
||||
|
||||
def proxy_response(target_url, content_type_override=None):
|
||||
def build_upstream_headers(referer):
|
||||
headers = {
|
||||
'User-Agent': request.headers.get('User-Agent'),
|
||||
'Accept': request.headers.get('Accept'),
|
||||
'Accept-Language': request.headers.get('Accept-Language'),
|
||||
'Accept-Encoding': request.headers.get('Accept-Encoding'),
|
||||
'Referer': referer,
|
||||
'Origin': referer,
|
||||
}
|
||||
|
||||
# Pass through fetch metadata and client hints when present
|
||||
for key in (
|
||||
'Sec-Fetch-Mode', 'Sec-Fetch-Site', 'Sec-Fetch-Dest', 'Sec-Fetch-User',
|
||||
'Sec-CH-UA', 'Sec-CH-UA-Mobile', 'Sec-CH-UA-Platform', 'DNT'
|
||||
):
|
||||
if key in request.headers:
|
||||
headers[key] = request.headers[key]
|
||||
|
||||
if forward_cookies and 'Cookie' in request.headers:
|
||||
headers['Cookie'] = request.headers['Cookie']
|
||||
dbg("forwarding cookies")
|
||||
|
||||
# Remove keys with None values
|
||||
return {k: v for k, v in headers.items() if v}
|
||||
|
||||
def proxy_response(target_url, content_type_override=None, referer_override=None):
|
||||
# Extract the base domain to spoof the referer
|
||||
referer_override = request.args.get('referer')
|
||||
request_referer = request.args.get('referer')
|
||||
if referer_override:
|
||||
referer = referer_override
|
||||
elif request_referer:
|
||||
referer = request_referer
|
||||
else:
|
||||
parsed_uri = urllib.parse.urlparse(target_url)
|
||||
referer = f"{parsed_uri.scheme}://{parsed_uri.netloc}/"
|
||||
dbg(f"proxy_response target={target_url} referer={referer}")
|
||||
|
||||
safe_request_headers = {
|
||||
'User-Agent': request.headers.get('User-Agent'),
|
||||
'Referer': referer, # Vital for bypassing CDN blocks
|
||||
'Origin': referer
|
||||
}
|
||||
safe_request_headers = build_upstream_headers(referer)
|
||||
|
||||
# Pass through Range headers so the browser can 'sniff' the video
|
||||
if 'Range' in request.headers:
|
||||
safe_request_headers['Range'] = request.headers['Range']
|
||||
|
||||
resp = session.get(target_url, headers=safe_request_headers, stream=True, timeout=30, allow_redirects=True)
|
||||
if debug_enabled:
|
||||
dbg(f"upstream status={resp.status_code} content_type={resp.headers.get('Content-Type')} content_length={resp.headers.get('Content-Length')}")
|
||||
|
||||
hop_by_hop = {
|
||||
'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization',
|
||||
@@ -201,6 +238,7 @@ def stream_video():
|
||||
|
||||
if content_type_override:
|
||||
forwarded_headers.append(('Content-Type', content_type_override))
|
||||
dbg(f"content_type_override={content_type_override}")
|
||||
|
||||
if request.method == 'HEAD':
|
||||
resp.close()
|
||||
@@ -217,10 +255,12 @@ def stream_video():
|
||||
return Response(generate(), status=resp.status_code, headers=forwarded_headers)
|
||||
|
||||
def proxy_hls_playlist(playlist_url, referer_hint=None):
|
||||
headers = {
|
||||
'User-Agent': request.headers.get('User-Agent', 'Mozilla/5.0'),
|
||||
'Accept': request.headers.get('Accept', '*/*')
|
||||
}
|
||||
dbg(f"proxy_hls_playlist url={playlist_url} referer_hint={referer_hint}")
|
||||
headers = build_upstream_headers(referer_hint or "")
|
||||
if 'User-Agent' not in headers:
|
||||
headers['User-Agent'] = 'Mozilla/5.0'
|
||||
if 'Accept' not in headers:
|
||||
headers['Accept'] = '*/*'
|
||||
resp = session.get(playlist_url, headers=headers, timeout=30)
|
||||
resp.raise_for_status()
|
||||
base_url = resp.url
|
||||
@@ -256,6 +296,7 @@ def stream_video():
|
||||
|
||||
if is_hls(video_url):
|
||||
try:
|
||||
dbg("detected input as hls")
|
||||
referer_hint = request.args.get('referer')
|
||||
if not referer_hint:
|
||||
parsed = urllib.parse.urlparse(video_url)
|
||||
@@ -266,6 +307,7 @@ def stream_video():
|
||||
|
||||
if is_direct_media(video_url):
|
||||
try:
|
||||
dbg("detected input as direct media")
|
||||
return proxy_response(video_url)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -273,22 +315,20 @@ def stream_video():
|
||||
try:
|
||||
# Configure yt-dlp options
|
||||
ydl_opts = {
|
||||
# 'format': 'best[protocol*=m3u8]/best[ext=mp4]/best',
|
||||
# Prefer HLS when available to enable chunked streaming in the browser.
|
||||
'format': 'best[protocol*=m3u8]/best[ext=mp4]/best',
|
||||
'format_sort': ['res', 'fps', 'vcodec:avc1', 'acodec:aac'],
|
||||
'quiet': False,
|
||||
'no_warnings': False,
|
||||
'socket_timeout': 30,
|
||||
'retries': 3,
|
||||
'fragment_retries': 3,
|
||||
'http_headers': {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
},
|
||||
'skip_unavailable_fragments': True
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
# Extract the info
|
||||
info = ydl.extract_info(video_url, download=False)
|
||||
dbg(f"yt_dlp extractor={info.get('extractor')} protocol={info.get('protocol')}")
|
||||
|
||||
# Try to get the URL from the info dict (works for progressive downloads)
|
||||
stream_url = info.get('url')
|
||||
@@ -311,17 +351,22 @@ def stream_video():
|
||||
if not referer_hint:
|
||||
parsed = urllib.parse.urlparse(video_url)
|
||||
referer_hint = f"{parsed.scheme}://{parsed.netloc}/"
|
||||
dbg(f"resolved stream_url={stream_url} referer_hint={referer_hint}")
|
||||
|
||||
if protocol and 'm3u8' in protocol:
|
||||
dbg("protocol indicates hls")
|
||||
return proxy_hls_playlist(stream_url, referer_hint)
|
||||
|
||||
if is_hls(stream_url):
|
||||
dbg("stream_url is hls")
|
||||
return proxy_hls_playlist(stream_url, referer_hint)
|
||||
|
||||
if is_dash(stream_url):
|
||||
return proxy_response(stream_url, content_type_override='application/dash+xml')
|
||||
dbg("stream_url is dash")
|
||||
return proxy_response(stream_url, content_type_override='application/dash+xml', referer_override=referer_hint)
|
||||
|
||||
return proxy_response(stream_url)
|
||||
dbg("stream_url is direct media")
|
||||
return proxy_response(stream_url, referer_override=referer_hint)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
Reference in New Issue
Block a user