From d35bbb333552c494edfdb7cc25bdf40fec56f511 Mon Sep 17 00:00:00 2001 From: Joakim Sindholt Date: Sat, 22 Mar 2025 19:00:14 +0100 Subject: [PATCH] reimplement stream selection --- .../resource.language.en_gb/strings.po | 33 +-- resources/settings.xml | 32 +-- service.py | 192 ++++++++---------- 3 files changed, 88 insertions(+), 169 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index d26945df..838a27cf 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -52,35 +52,6 @@ msgctxt "#33020" msgid "Use DASH manifest builder (kodi 19+ only) (experimental)" msgstr "" -msgctxt "#33030" -msgid "Maximum resolution" +msgctxt "#33050" +msgid "youtube-dl format options" msgstr "" - -msgctxt "#33031" -msgid "7680x4320 (8k)" -msgstr "" - -msgctxt "#33032" -msgid "3840x2160 (4k)" -msgstr "" - -msgctxt "#33033" -msgid "2560x1440 (2k)" -msgstr "" - -msgctxt "#33034" -msgid "1920x1080 (FHD)" -msgstr "" - -msgctxt "#33035" -msgid "1280x720 (HD)" -msgstr "" - -msgctxt "#33036" -msgid "854x480 (SD)" -msgstr "" - -msgctxt "#33040" -msgid "Prefer AVC1/H.264/MP4" -msgstr "" - diff --git a/resources/settings.xml b/resources/settings.xml index b02959e4..e680073f 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -29,35 +29,15 @@ true - + 0 - - - true - - - 1920 + - - - - - - - - + true - - - - 0 - - - true - - - false - + + 33050 + diff --git a/service.py b/service.py index 91f23c51..97fa9a56 100644 --- a/service.py +++ b/service.py @@ -79,54 +79,6 @@ def getParams(): return result -def extract_manifest_url(result): - # sometimes there is an url directly - # but for some extractors this is only one quality and sometimes not even a real manifest - if 'manifest_url' in result and get_adaptive_type_from_url(result['manifest_url']): - return result['manifest_url'] - # otherwise we must relay that the requested formats have been found and - # extract the manifest url from them - if 'requested_formats' not in result: - return None - for entry in result['requested_formats']: - # the resolver marks not all entries with video AND audio - # but usually adaptive video streams also have audio - if 'manifest_url' in entry and 'vcodec' in entry and get_adaptive_type_from_url(entry['manifest_url']): - return entry['manifest_url'] - return None - - -def extract_best_all_in_one_stream(result): - # Check if 'formats' key exists in result - if 'formats' not in result: - return None - # if there is nothing to choose from simply take the shot it is correct - if len(result['formats']) == 1: - return result['formats'][0]['url'] - audio_video_streams = [] - filter_format = (lambda f: f.get('vcodec', 'none') != 'none' and f.get('acodec', 'none') != 'none') - # assume it is a video containg audio. Get the one with the highest resolution - for entry in result['formats']: - if filter_format(entry): - audio_video_streams.append(entry) - if audio_video_streams: - return max(audio_video_streams, key=lambda f: f['width'])['url'] - # test if it is an audio only stream - if result.get('vcodec', 'none') == 'none': - # in case of multiple audio streams get the best - audio_streams = [] - filter_format = (lambda f: f.get('abr', 'none') != 'none') - for entry in result['formats']: - if filter_format(entry): - audio_streams.append(entry) - if audio_streams: - return max(audio_streams, key=lambda f: f['abr'])['url'] - # not all extractors provide an abr (and other fields are also not guaranteed), try to get any audio - if (entry.get('acodec', 'none') != 'none') or entry.get('ext', False) in ['mp3', 'wav', 'opus']: - return entry['url'] - # was not able to resolve - return None - def get_adaptive_type_from_url(url): supported_endings = [".m3u8", ".hls", ".mpd", ".rtmp", ".ism"] file = url.split('/')[-1] @@ -151,62 +103,91 @@ def check_if_kodi_supports_manifest(url): log(msg=msg, level=xbmc.LOGWARNING) return adaptive_type, supported -def build_dash_manifest(result): - if not usedashbuilder: - return None - if 'requested_formats' not in result: - return None - if len(result['requested_formats']) != 2: - return None - video_format = result['requested_formats'][0] - audio_format = result['requested_formats'][1] - # Currently only support YouTube - if '.googlevideo.com' not in video_format['url']: - return None - if (video_format['acodec'] != 'none') or (audio_format['vcodec'] != 'none'): - return None - if ('container' not in video_format) or ('container' not in audio_format): - return None - if (video_format['container'] != "mp4_dash") and (video_format['container'] != "webm_dash"): - return None - if (audio_format['container'] != "m4a_dash") and (audio_format['container'] != "webm_dash"): - return None - - import dash_builder as dash - builder = dash.Manifest(result['duration']) - builder.add_video_format(video_format) - builder.add_audio_format(audio_format) - manifest = builder.emit() - dash_url = dash.start_httpd(manifest) - log(f"Generated DASH manifest at {dash_url}") - return dash_url - def createListItemFromVideo(result): debug(result) + + url = None adaptive_type = False - if xbmcplugin.getSetting(int(sys.argv[1]),"usemanifest") == 'true': - url = extract_manifest_url(result) + + requested_formats = result.get('requested_formats', result.get('requested_downloads')) + have_requested_formats = requested_formats is not None and len(requested_formats) > 0 + + if url is None and usemanifest: + if have_requested_formats: + url = requested_formats[0].get('manifest_url') + if url is None: + url = result.get('manifest_url') if url is not None: - log("found original manifest: " + url) adaptive_type, supported = check_if_kodi_supports_manifest(url) if not supported: url = None - if url is None: - url = build_dash_manifest(result) - if url is not None: + + if url is None and have_requested_formats: + want_video = False + want_audio = False + video_format = None + audio_format = None + joined_formats = [] + for f in reversed(requested_formats): + vcodec = f.get('vcodec', "none") + acodec = f.get('acodec', "none") + container = f.get('container', "") + if vcodec != "none": + want_video = True + if acodec != "none": + want_audio = True + + if vcodec != "none" and acodec != "none": + if 'url' in f: + joined_formats.append(f['url']) + elif vcodec != "none": + if video_format is None and container in ["mp4_dash", "webm_dash"]: + video_format = f + elif acodec != "none": + if audio_format is None and container in ["m4a_dash", "webm_dash"]: + audio_format = f + if usedashbuilder and (want_audio or want_video) and (not want_audio or audio_format is not None) and (not want_video or video_format is not None): + import dash_builder as dash + builder = dash.Manifest(result.get('duration', "0")) + fail = False + if video_format is not None: + try: + builder.add_video_format(video_format) + except Exception as e: + log(f"DASH builder failed to add video stream: {e}") + fail = True + if audio_format is not None: + try: + builder.add_audio_format(audio_format) + except Exception as e: + log(f"DASH builder failed to add audio stream: {e}") + fail = True + if not fail: + url = dash.start_httpd(builder.emit()) adaptive_type, supported = check_if_kodi_supports_manifest(url) if not supported: url = None + else: + log(f"Generated DASH manifest at {url}") if url is None: - log("could not find an original manifest or manifest is not supported falling back to best all-in-one stream") - url = extract_best_all_in_one_stream(result) - if url is None: - err_msg = "Error: was not able to extract manifest or all-in-one stream. Implement https://github.com/firsttris/plugin.video.sendtokodi/issues/34" - log(err_msg) - showInfoNotification(err_msg) - raise Exception(err_msg) - else: - url = result['url'] + for u in joined_formats: + adaptive_type, supported = check_if_kodi_supports_manifest(u) + if supported: + url = u + break + + if url is None: + url = result.get('url') + adaptive_type = False + + if url is None: + msg = "Error: unable to extract stream URL" + if have_requested_formats: + msg = "Error: no supported stream URLs found" + log(msg) + showInfoNotification(msg) + raise Exception(msg) + log("creating list item for url {}".format(url)) list_item = xbmcgui.ListItem(result['title'], path=url) video_info = list_item.getVideoInfoTag() @@ -302,7 +283,6 @@ patch_strptime() # Pass in 'in_playlist' to only show this behavior for # playlist items. ydl_opts = { - 'format': 'best', 'extract_flat': 'in_playlist' } @@ -310,24 +290,12 @@ params = getParams() url = str(params['url']) ydl_opts.update(params['ydlOpts']) -if xbmcplugin.getSetting(int(sys.argv[1]),"usemanifest") == 'true': - ydl_opts['format'] = 'bestvideo*+bestaudio/best' - -usedashbuilder = (xbmcplugin.getSetting(int(sys.argv[1]),"usedashbuilder") == 'true') and (sys.version_info[0] >= 3) -if usedashbuilder: - maxresolution = xbmcplugin.getSetting(int(sys.argv[1]), "maxresolution") - preferavc1 = (xbmcplugin.getSetting(int(sys.argv[1]), "preferavc1") == 'true') - - if preferavc1: - vcodec = '[vcodec*=avc1]' - else: - vcodec = '' - - ydl_opts['format'] = f'bv{vcodec}[width<={maxresolution}]+ba/' - ydl_opts['format'] += f'bv[width<={maxresolution}]+ba/' - ydl_opts['format'] += f'b{vcodec}[width<={maxresolution}]/' - ydl_opts['format'] += f'b[width<={maxresolution}]/' - ydl_opts['format'] += f'b*' +usemanifest = xbmcplugin.getSetting(int(sys.argv[1]),"usemanifest") == "true" +usedashbuilder = xbmcplugin.getSetting(int(sys.argv[1]),"usedashbuilder") == "true" and sys.version_info[0] >= 3 +ytdlformat = xbmcplugin.getSetting(int(sys.argv[1]), "ytdlformat"); +if ytdlformat == "": + ytdlformat = "bv*+ba/b" if usemanifest or usedashbuilder else "b" +ydl_opts['format'] = ytdlformat; ydl = YoutubeDL(ydl_opts) ydl.add_default_info_extractors() -- 2.48.1