fix: update innertube clients and fix HLS/DASH quality switching
All checks were successful
CI / test (push) Successful in 53s

- Update innertube client versions to match yt-dlp (android 21.02.35,
  ios 21.02.3, web 2.20260114.08.00, android_vr 1.65.10)
- Remove obsolete clients (android-test-suite, ios_vr)
- Replace tv_embedded with TVHTML5_SIMPLY (cn 75)
- Add new clients: web_embedded, mweb, tv
- Fix HLS freeze on quality switch: use nextLevel instead of
  currentLevel, handle bufferStalledError, stream proxy segments
  instead of buffering in memory
- Populate DASH quality selector with actual sources (no Auto)
- Render quality-select empty in template, let JS populate per mode
This commit is contained in:
2026-05-03 12:32:55 -05:00
parent 50ad959a80
commit 8d66143c90
9 changed files with 467 additions and 246 deletions

View File

@@ -17,8 +17,16 @@ from flask import request
import youtube
from youtube import yt_app
from youtube import util, comments, local_playlist, yt_data_extract
from youtube import watch_formats
import settings
# Backward compatibility aliases
codec_name = watch_formats.codec_name
video_quality_string = watch_formats.video_quality_string
short_video_quality_string = watch_formats.short_video_quality_string
audio_quality_string = watch_formats.audio_quality_string
format_bytes = watch_formats.format_bytes
logger = logging.getLogger(__name__)
@@ -29,15 +37,7 @@ except FileNotFoundError:
decrypt_cache = {}
def codec_name(vcodec):
if vcodec.startswith('avc'):
return 'h264'
elif vcodec.startswith('av01'):
return 'av1'
elif vcodec.startswith('vp'):
return 'vp'
else:
return 'unknown'
# codec_name imported from watch_formats
def get_video_sources(info, target_resolution):
@@ -446,7 +446,7 @@ def extract_info(video_id, use_invidious, playlist_id=None, index=None):
info['hls_audio_tracks'] = {}
hls_data = None
hls_client_used = None
for hls_client in ('ios', 'ios_vr', 'android'):
for hls_client in ('ios', 'android'):
try:
resp = fetch_player_response(hls_client, video_id) or {}
hls_data = json.loads(resp) if isinstance(resp, str) else resp
@@ -621,55 +621,10 @@ def extract_info(video_id, use_invidious, playlist_id=None, index=None):
return info
def video_quality_string(format):
if format['vcodec']:
result = f"{format['width'] or '?'}x{format['height'] or '?'}"
if format['fps']:
result += f" {format['fps']}fps"
return result
elif format['acodec']:
return 'audio only'
return '?'
def short_video_quality_string(fmt):
result = f"{fmt['quality'] or '?'}p"
if fmt['fps']:
result += str(fmt['fps'])
if fmt['vcodec'].startswith('av01'):
result += ' AV1'
elif fmt['vcodec'].startswith('avc'):
result += ' h264'
else:
result += f" {fmt['vcodec']}"
return result
def audio_quality_string(fmt):
if fmt['acodec']:
if fmt['audio_bitrate']:
result = f"{fmt['audio_bitrate']}k"
else:
result = '?k'
if fmt['audio_sample_rate']:
result += f" {'%.3G' % (fmt['audio_sample_rate']/1000)}kHz"
return result
elif fmt['vcodec']:
return 'video only'
return '?'
# from https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/utils.py
def format_bytes(bytes):
if bytes is None:
return 'N/A'
if type(bytes) is str:
bytes = float(bytes)
if bytes == 0.0:
exponent = 0
else:
exponent = int(math.log(bytes, 1024.0))
# video_quality_string imported from watch_formats
# short_video_quality_string imported from watch_formats
# audio_quality_string imported from watch_formats
# format_bytes imported from watch_formats
suffix = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'][exponent]
converted = float(bytes) / float(1024 ** exponent)
return '%.2f%s' % (converted, suffix)
@@ -832,14 +787,12 @@ def get_audio_track():
# This is an actual segment - fetch and serve it
try:
headers = (
('User-Agent', 'Mozilla/5.0'),
('Accept', '*/*'),
)
content = util.fetch_url(seg_url, headers=headers,
debug_name='hls_seg', report_text=None)
headers_dict = {
'User-Agent': 'Mozilla/5.0',
'Accept': '*/*',
}
# Determine content type based on URL or content
# Determine content type based on URL
# HLS segments are usually MPEG-TS (.ts) but can be MP4 (.mp4, .m4s)
if '.mp4' in seg_url or '.m4s' in seg_url or seg_url.lower().endswith('.mp4'):
content_type = 'video/mp4'
@@ -849,7 +802,23 @@ def get_audio_track():
# Default to MPEG-TS for HLS
content_type = 'video/mp2t'
return flask.Response(content, mimetype=content_type,
response, cleanup_func = util.fetch_url_response(
seg_url, headers=tuple(headers_dict.items()),
timeout=30, use_tor=settings.route_tor)
def generate():
try:
while True:
chunk = response.read(64 * 1024) # 64 KB chunks
if not chunk:
break
yield chunk
finally:
cleanup_func(response)
return flask.Response(
flask.stream_with_context(generate()),
mimetype=content_type,
headers={
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',