From 8d66143c90c4b86e8ec8dfed67753bef2abf2114 Mon Sep 17 00:00:00 2001 From: Astounds Date: Sun, 3 May 2026 12:32:55 -0500 Subject: fix: update innertube clients and fix HLS/DASH quality switching - 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 --- youtube/watch.py | 103 +++++++++++++++++++------------------------------------ 1 file changed, 36 insertions(+), 67 deletions(-) (limited to 'youtube/watch.py') diff --git a/youtube/watch.py b/youtube/watch.py index 9d1e442..ec446f4 100644 --- a/youtube/watch.py +++ b/youtube/watch.py @@ -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', -- cgit v1.2.3