From d1ef005b092cfab54528bc14a820959ef7a9cf5c Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 18 May 2023 06:52:45 +1000 Subject: [PATCH 01/13] Update itag format details --- .../kodion/impl/xbmc/xbmc_context.py | 1 + .../youtube/helper/video_info.py | 55 +++++++++++++------ 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py index 867fb190e..2f3e4630a 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py @@ -308,6 +308,7 @@ def inputstream_adaptive_capabilities(self, capability=None): capability_map = { 'live': '2.0.12', + 'dts': '2.1.15', 'drm': '2.2.12', 'vp9': '2.3.14', 'vp9.2': '2.3.14', diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index acd0545be..af12e8e73 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -290,9 +290,10 @@ class VideoInfo(object): '271': {'container': 'webm', 'dash/video': True, 'video': {'height': 1440, 'encoding': 'vp9'}}, - '272': {'container': 'webm', + '272': {'container': 'webm', # was VP9 2160p30 'dash/video': True, - 'video': {'height': 2160, 'encoding': 'vp9'}}, + 'fps': 60, + 'video': {'height': 4320, 'encoding': 'vp9'}}, '278': {'container': 'webm', 'dash/video': True, 'video': {'height': 144, 'encoding': 'vp9'}}, @@ -395,6 +396,14 @@ class VideoInfo(object): 'dash/video': True, 'fps': 30, 'video': {'height': 2160, 'encoding': 'av1'}}, + '402': {'container': 'mp4', + 'dash/video': True, + 'fps': 30, + 'video': {'height': 4320, 'encoding': 'av1'}}, + '571': {'container': 'mp4', + 'dash/video': True, + 'fps': 30, + 'video': {'height': 4320, 'encoding': 'av1'}}, '694': {'container': 'mp4', 'dash/video': True, 'fps': 60, @@ -443,39 +452,39 @@ class VideoInfo(object): # === Dash (audio only) '139': {'container': 'mp4', 'sort': [48, 0], - 'title': 'aac@48', + 'title': 'he-aac@48', 'dash/audio': True, 'audio': {'bitrate': 48, 'encoding': 'aac'}}, '140': {'container': 'mp4', 'sort': [129, 0], - 'title': 'aac@128', + 'title': 'aac-lc@128', 'dash/audio': True, 'audio': {'bitrate': 128, 'encoding': 'aac'}}, '141': {'container': 'mp4', 'sort': [143, 0], - 'title': 'aac@256', + 'title': 'aac-lc@256', 'dash/audio': True, 'audio': {'bitrate': 256, 'encoding': 'aac'}}, '256': {'container': 'mp4', - 'title': 'aac/itag 256', + 'title': 'he-aac@192', 'dash/audio': True, - 'unsupported': True, - 'audio': {'bitrate': 0, 'encoding': 'aac'}}, + 'audio': {'bitrate': 192, 'encoding': 'aac'}}, '258': {'container': 'mp4', - 'title': 'aac/itag 258', + 'title': 'aac-LC@384', 'dash/audio': True, - 'unsupported': True, - 'audio': {'bitrate': 0, 'encoding': 'aac'}}, + 'audio': {'bitrate': 384, 'encoding': 'aac'}}, '325': {'container': 'mp4', - 'title': 'dtse/itag 325', + 'title': 'dtse@384', 'dash/audio': True, - 'unsupported': True, - 'audio': {'bitrate': 0, 'encoding': 'aac'}}, + 'audio': {'bitrate': 384, 'encoding': 'dtse'}}, + '327': {'container': 'mp4', + 'title': 'aac-lc@256', + 'dash/audio': True, + 'audio': {'bitrate': 256, 'encoding': 'aac'}}, '328': {'container': 'mp4', - 'title': 'ec-3/itag 328', + 'title': 'ec-3@384', 'dash/audio': True, - 'unsupported': True, - 'audio': {'bitrate': 0, 'encoding': 'aac'}}, + 'audio': {'bitrate': 384, 'encoding': 'ec-3'}}, '171': {'container': 'webm', 'sort': [128, 0], 'title': 'vorbis@128', @@ -501,6 +510,15 @@ class VideoInfo(object): 'title': 'opus@160', 'dash/audio': True, 'audio': {'bitrate': 160, 'encoding': 'opus'}}, + '338': {'container': 'webm', + 'sort': [141, 0], + 'title': 'opus@480', + 'dash/audio': True, + 'audio': {'bitrate': 480, 'encoding': 'opus'}}, + '380': {'container': 'mp4', + 'title': 'ac-3@384', + 'dash/audio': True, + 'audio': {'bitrate': 384, 'encoding': 'ac-3'}}, # === DASH adaptive audio only '9997': {'container': 'mpd', 'sort': [-1, 0], @@ -1369,7 +1387,8 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): if ((quality_type and container != quality_type) or (mime_type == 'video/webm' and not {'vp9', 'vp9.2'} & ia_capabilities) or (mime_type == 'audio/webm' and not {'vorbis', 'opus'} & ia_capabilities) - or (codec in {'av01', 'av1'} and 'av1' not in ia_capabilities)): + or (codec in {'av01', 'av1'} and 'av1' not in ia_capabilities) + or (codec.startswith('dts') and 'dts' not in ia_capabilities)): continue if 'audioTrack' in stream_map: From f79e3ab62f982db70d3c6cd1b6ba119e3950aa85 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 24 May 2023 11:44:26 +1000 Subject: [PATCH 02/13] Make case of stream title consistent with other titles --- resources/lib/youtube_plugin/youtube/helper/video_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index af12e8e73..2ceaec0b0 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -470,7 +470,7 @@ class VideoInfo(object): 'dash/audio': True, 'audio': {'bitrate': 192, 'encoding': 'aac'}}, '258': {'container': 'mp4', - 'title': 'aac-LC@384', + 'title': 'aac-lc@384', 'dash/audio': True, 'audio': {'bitrate': 384, 'encoding': 'aac'}}, '325': {'container': 'mp4', From ec0b0b4748c31a39df6d25ca6a78fe0a4340a918 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 May 2023 12:07:19 +1000 Subject: [PATCH 03/13] Fix possible regression in capability evaluation - The meaning of None has changed a few times (#127, #146) - Add comment to define possible values - Vorbis audio doesn't seem to be offered by Youtube anymore - Vorbis appears to have been supported by ISA for a long time --- .../lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py index 2f3e4630a..7a933521b 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py @@ -306,13 +306,17 @@ def inputstream_adaptive_capabilities(self, capability=None): if not use_dash or not inputstream_version: return frozenset() if capability is None else None + # Values of capability map can be any of the following: + # - required version number as string for comparison with actual installed InputStream.Adaptive version + # - any Falsey value to exclude capability regardless of version + # - True to include capability regardless of version capability_map = { 'live': '2.0.12', 'dts': '2.1.15', 'drm': '2.2.12', 'vp9': '2.3.14', 'vp9.2': '2.3.14', - 'vorbis': None, + 'vorbis': '2.3.14', 'opus': '19.0.7', 'av1': '20.3.0', } @@ -320,7 +324,8 @@ def inputstream_adaptive_capabilities(self, capability=None): if capability is None: ia_loose_version = utils.loose_version(inputstream_version) capabilities = frozenset(key for key, version in capability_map.items() - if version and ia_loose_version >= utils.loose_version(version)) + if version is True + or version and ia_loose_version >= utils.loose_version(version)) return capabilities return capability_map.get(capability) From 3a3d0882c1a9c8276bf4b080fc6d4e2fd5541726 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 May 2023 22:06:46 +1000 Subject: [PATCH 04/13] Allow HDR when Adaptive MP4 video is selected --- .../lib/youtube_plugin/kodion/impl/abstract_settings.py | 2 -- resources/settings.xml | 5 +---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py index 6a2e28870..fee535381 100644 --- a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py @@ -142,8 +142,6 @@ def use_dash_videos(self): return self.get_bool(constants.setting.DASH_VIDEOS, False) def include_hdr(self): - if self.get_mpd_quality() == 'mp4': - return False return self.get_bool(constants.setting.DASH_INCL_HDR, False) def use_dash_live_streams(self): diff --git a/resources/settings.xml b/resources/settings.xml index 14bfc4524..ec002b14b 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -268,10 +268,7 @@ false - - true - 8 - + true From c306ab7f8d3ea8b4f09c66e209975ac49c8cbdbe Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 10 Jun 2023 17:10:41 +1000 Subject: [PATCH 05/13] Make lang attribute value conform to MPD specs Also display language in stream quality selector --- .../lib/youtube_plugin/youtube/helper/video_info.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 2ceaec0b0..a7a3f9760 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1281,7 +1281,7 @@ def _method_get_video_info(self): stream_details['audio']['bitrate'] = bitrate if not video_info: stream_details['title'] = '{0}@{1}'.format(codec, bitrate) - if audio_info['lang']: + if audio_info['lang'] not in {'', 'und'}: stream_details['title'] += ' Multi-language' video_stream.update(stream_details) @@ -1316,6 +1316,8 @@ def parse_to_stream_list(streams): 'headers': curl_headers, 'playback_stats': playback_stats} stream.update(yt_format) + if 'audioTrack' in stream_map: + stream['title'] = '{0} {1}'.format(stream['title'], stream_map['audioTrack']['displayName']) stream_list.append(stream) # extract streams from map @@ -1393,7 +1395,7 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): if 'audioTrack' in stream_map: audio_track = stream_map['audioTrack'] - language_code = audio_track.get('id', '')[0:2] + language_code = audio_track.get('id', '')[0:2] or 'und' label = audio_track.get('displayName', '') audio_type = 'main' if audio_track.get('audioIsDefault') else 'dub' height = None @@ -1402,8 +1404,8 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): if language_code == self._language_base: preferred_audio = '_'+language_code elif media_type == 'audio': - language_code = '' - label = '' + language_code = 'und' + label = stream_map.get('audioQuality', '') audio_type = 'main' height = None width = None From bc2522c5fb8b5f374dae275b58557885a6c82de3 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 11 Jun 2023 03:05:59 +1000 Subject: [PATCH 06/13] Improve handling of dubbed and descriptive audio Also align MPD manifest with spec and ISA quirks --- .../youtube/helper/video_info.py | 103 ++++++++++++------ 1 file changed, 72 insertions(+), 31 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index a7a3f9760..2c697d3d0 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1357,7 +1357,11 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): } data = {} - preferred_audio = '' + preferred_audio = { + 'id': '', + 'language_code': None, + 'audio_type': 0, + } for stream_map in adaptive_fmts: mime_type = stream_map.get('mimeType') if not mime_type: @@ -1395,25 +1399,50 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): if 'audioTrack' in stream_map: audio_track = stream_map['audioTrack'] - language_code = audio_track.get('id', '')[0:2] or 'und' + language = audio_track.get('id', '') + if '.' in language: + language_code, audio_type = language.split('.') + audio_type = int(audio_type) + else: + language_code = language or 'und' + audio_type = 4 label = audio_track.get('displayName', '') - audio_type = 'main' if audio_track.get('audioIsDefault') else 'dub' + if audio_type == 4 or audio_track.get('audioIsDefault'): + audio_role = 'main' + elif audio_type == 3: + audio_role = 'dub' + elif audio_type == 2: + audio_role = 'description' + # Unsure of what other audio types are actually available + # Role set to "alternate" as default fallback + else: + audio_role = 'alternate' height = None width = None - key = '{0}_{1}'.format(mime_type, language_code) - if language_code == self._language_base: - preferred_audio = '_'+language_code + key = '{0}_{1}'.format(mime_type, language) + if (language_code == self._language_base and ( + not preferred_audio['id'] + or audio_role == 'main' + or audio_type > preferred_audio['audio_type'] + )): + preferred_audio = { + 'id': '_'+language, + 'language_code': language_code, + 'audio_type': audio_type, + } elif media_type == 'audio': language_code = 'und' label = stream_map.get('audioQuality', '') - audio_type = 'main' + audio_role = 'main' height = None width = None key = mime_type else: + # Could use "zxx" language code for Non-Linguistic, Not Applicable + # but that is too verbose language_code = '' label = stream_map.get('qualityLabel', '') - audio_type = None + audio_role = None height = stream_map.get('height') width = stream_map.get('width') key = mime_type @@ -1454,7 +1483,7 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): 'indexRange': '{start}-{end}'.format(**index_range), 'initRange': '{start}-{end}'.format(**init_range), 'lang': language_code, - 'audioType': audio_type, + 'audioRole': audio_role, 'sampleRate': int(stream_map.get('audioSampleRate', '0'), 10), 'channels': stream_map.get('audioChannels'), } @@ -1494,8 +1523,8 @@ def _stream_sort(stream): else: stream_info['video'] = data['video/webm'][0] - mp4_audio = data.get('audio/mp4'+preferred_audio, [None])[0] - webm_audio = data.get('audio/webm'+preferred_audio, [None])[0] + mp4_audio = data.get('audio/mp4'+preferred_audio['id'], [None])[0] + webm_audio = data.get('audio/webm'+preferred_audio['id'], [None])[0] if _stream_sort(mp4_audio) > _stream_sort(webm_audio): stream_info['audio'] = mp4_audio else: @@ -1513,21 +1542,24 @@ def _stream_sort(stream): for streams in data.values(): default = False original = False + impaired = False + label = '' main_stream = streams[0] media_type = main_stream['mediaType'] if media_type == 'video': - # InputStream.Adaptive seems to mark any video AdaptationSet as default - # regardless of Role or default attribute if stream_info[media_type] == main_stream: default = True - video_type = 'main' + role = 'main' else: - video_type = 'alternate' + role = 'alternate' original = '' elif media_type == 'audio': label = main_stream['label'] - audio_type = main_stream['audioType'] - original = audio_type == 'main' + role = main_stream['audioRole'] + if role == 'main': + original = True + elif role == 'description': + impaired = True if stream_info[media_type] == main_stream: default = True # Use main audio stream with same container format as video stream @@ -1542,8 +1574,16 @@ def _stream_sort(stream): ' id="', str(set_id), '"' ' mimeType="', main_stream['mimeType'], '"' ' lang="', main_stream['lang'], '"' + # name attribute is ISA specific and does not exist in the MPD spec + # Should be a child Label element instead + ' name="', label, '"' + # original, default and impaired are ISA specific attributes ' original="', str(original).lower(), '"' - ' default="', str(default).lower(), '">\n' + ' default="', str(default).lower(), '"' + ' impaired="', str(impaired).lower(), '">\n' + # AdaptationSet Label element not currently used by ISA + '\t\t\t\n' + '\t\t\t\n' )) if license_url: @@ -1554,35 +1594,36 @@ def _stream_sort(stream): '\t\t\t\n' )) + num_streams = len(streams) if media_type == 'audio': - # InputStream.Adaptive seems to mark any AdaptationSet with a Role as default - # regardless of what the role is. Omit Role as a workaround - # out_list.extend(( - # '\t\t\t\n', - # '\t\t\t\n' - # )) out_list.extend((( - '\t\t\t\n' + '\t\t\t\n' '\t\t\t\t\n' + # Representation Label element not currently used by ISA + '\t\t\t\t\n' '\t\t\t\t{baseUrl}\n' '\t\t\t\t\n' '\t\t\t\t\t\n' '\t\t\t\t\n' '\t\t\t\n' - ).format(**stream) for stream in streams)) + ).format(quality=(idx + 1), priority=(num_streams - idx), **stream) for idx, stream in enumerate(streams))) elif media_type == 'video': - out_list.extend(( - '\t\t\t\n' - )) out_list.extend((( - '\t\t\t\n' + '\t\t\t\n' + # Representation Label element not currently used by ISA '\t\t\t\t\n' '\t\t\t\t{baseUrl}\n' '\t\t\t\t\n' '\t\t\t\t\t\n' '\t\t\t\t\n' '\t\t\t\n' - ).format(**stream) for stream in streams)) + ).format(quality=(idx + 1), priority=(num_streams - idx), **stream) for idx, stream in enumerate(streams))) out_list.append('\t\t\n') set_id += 1 From daccb437018a672a3a27ff6376581fdfd8393467 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 14 Jun 2023 10:43:24 +1000 Subject: [PATCH 07/13] Further improve stream labelling - Use custom video label (Youtube qualityLabel is not accurate) - Avoid duplicate tokens (from ISA and Youtube descriptions) - Provide bitrate of audio stream per AdaptationSet (not shown by ISA) --- .../youtube/helper/video_info.py | 128 ++++++++++-------- 1 file changed, 69 insertions(+), 59 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 2c697d3d0..5c56ecb70 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1343,7 +1343,7 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): mpd_quality = self._context.get_settings().get_mpd_quality() quality_type = isinstance(mpd_quality, str) and mpd_quality or '' quality_height = isinstance(mpd_quality, int) and mpd_quality or 0 - hdr = self._context.get_settings().include_hdr() and {'vp9.2', 'av1'} & ia_capabilities + include_hdr = self._context.get_settings().include_hdr() and {'vp9.2', 'av1'} & ia_capabilities limit_30fps = self._context.get_settings().mpd_30fps_limit() ipaddress = self._context.get_settings().httpd_listen() @@ -1397,67 +1397,76 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): or (codec.startswith('dts') and 'dts' not in ia_capabilities)): continue - if 'audioTrack' in stream_map: - audio_track = stream_map['audioTrack'] - language = audio_track.get('id', '') - if '.' in language: - language_code, audio_type = language.split('.') - audio_type = int(audio_type) - else: - language_code = language or 'und' - audio_type = 4 - label = audio_track.get('displayName', '') - if audio_type == 4 or audio_track.get('audioIsDefault'): - audio_role = 'main' - elif audio_type == 3: - audio_role = 'dub' - elif audio_type == 2: - audio_role = 'description' - # Unsure of what other audio types are actually available - # Role set to "alternate" as default fallback + if media_type == 'audio': + if 'audioTrack' in stream_map: + audio_track = stream_map['audioTrack'] + language = audio_track.get('id', '') + if '.' in language: + language_code, audio_type = language.split('.') + audio_type = int(audio_type) + else: + language_code = language or 'und' + audio_type = 4 + if audio_type == 4 or audio_track.get('audioIsDefault'): + role = 'main' + label = 'Original' + elif audio_type == 3: + role = 'dub' + label = 'Dubbed' + elif audio_type == 2: + role = 'description' + label = 'Descriptive' + # Unsure of what other audio types are actually available + # Role set to "alternate" as default fallback + else: + role = 'alternate' + label = 'Alternate' + label = '{0} - {1:.0f} kbps'.format(label, + stream_map.get('averageBitrate', 0) / 1000) + key = '{0}_{1}'.format(mime_type, language) + if (language_code == self._language_base and ( + not preferred_audio['id'] + or role == 'main' + or audio_type > preferred_audio['audio_type'] + )): + preferred_audio = { + 'id': '_'+language, + 'language_code': language_code, + 'audio_type': audio_type, + } else: - audio_role = 'alternate' - height = None - width = None - key = '{0}_{1}'.format(mime_type, language) - if (language_code == self._language_base and ( - not preferred_audio['id'] - or audio_role == 'main' - or audio_type > preferred_audio['audio_type'] - )): - preferred_audio = { - 'id': '_'+language, - 'language_code': language_code, - 'audio_type': audio_type, - } - elif media_type == 'audio': - language_code = 'und' - label = stream_map.get('audioQuality', '') - audio_role = 'main' - height = None - width = None - key = mime_type + language_code = 'und' + role = 'main' + label = 'Original - {0:.0f} kbps'.format(stream_map.get('averageBitrate', 0) / 1000) + key = mime_type + sample_rate = int(stream_map.get('audioSampleRate', '0'), 10) + channels = stream_map.get('audioChannels', 2) + height = width = fps = frame_rate = hdr = None else: # Could use "zxx" language code for Non-Linguistic, Not Applicable # but that is too verbose language_code = '' - label = stream_map.get('qualityLabel', '') - audio_role = None height = stream_map.get('height') + if 0 < quality_height < height: + continue width = stream_map.get('width') + # map frame rates to a more common representation to lessen the chance of double refresh changes + # sometimes 30 fps is 30 fps, more commonly it is 29.97 fps (same for all mapped frame rates) + fps = stream_map.get('fps', 0) + if limit_30fps and fps > 30: + continue + if fps: + frame_rate = '{0}/{1}'.format(fps * 1000, fps_scale_map.get(fps, 1000)) + else: + frame_rate = None + hdr = 'HDR' in stream_map.get('qualityLabel', '') + if hdr and not include_hdr: + continue + label = '{0}p{1}{2}'.format(height, + fps if fps > 30 else '', + ' HDR' if hdr else '') key = mime_type - - # map frame rates to a more common representation to lessen the chance of double refresh changes - # sometimes 30 fps is 30 fps, more commonly it is 29.97 fps (same for all mapped frame rates) - fps = stream_map.get('fps', 0) - if fps: - frame_rate = '{0}/{1}'.format(fps * 1000, fps_scale_map.get(fps, 1000)) - else: - frame_rate = None - - if ((not hdr and 'HDR' in label) or (limit_30fps and fps > 30) - or (height and 0 < quality_height < height)): - continue + role = sample_rate = channels = None if key not in data: data[key] = {} @@ -1480,12 +1489,13 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): 'bitrate': stream_map.get('bitrate', 0), 'fps': fps, 'frameRate': frame_rate, + 'hdr': hdr, 'indexRange': '{start}-{end}'.format(**index_range), 'initRange': '{start}-{end}'.format(**init_range), 'lang': language_code, - 'audioRole': audio_role, - 'sampleRate': int(stream_map.get('audioSampleRate', '0'), 10), - 'channels': stream_map.get('audioChannels'), + 'role': role, + 'sampleRate': sample_rate, + 'channels': channels, } if 'video/mp4' not in data and 'video/webm' not in data: @@ -1498,7 +1508,7 @@ def _stream_sort(stream): return ( stream['height'], stream['fps'], - hdr and ('HDR' in stream['label']), + stream['hdr'], # Prefer lower bitrate for video streams # Used to preference more advanced codecs -stream['bitrate'], @@ -1555,7 +1565,7 @@ def _stream_sort(stream): original = '' elif media_type == 'audio': label = main_stream['label'] - role = main_stream['audioRole'] + role = main_stream['role'] if role == 'main': original = True elif role == 'description': From 50e61a32c8f5d12a53623ec00c1a9ba58d62b626 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 16 Jun 2023 00:13:42 +1000 Subject: [PATCH 08/13] Remove conditions for 30 fps limit setting - Should not be linked to HDR setting as there are non-HDR HFR itags - Should not be disabled if a preferred container is selected --- .../lib/youtube_plugin/kodion/impl/abstract_settings.py | 2 -- resources/settings.xml | 9 ++------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py index fee535381..5dd184d7d 100644 --- a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py @@ -238,8 +238,6 @@ def mpd_video_qualities(self): return qualities def mpd_30fps_limit(self): - if self.include_hdr() or isinstance(self.get_mpd_quality(), str): - return False return self.get_bool(constants.setting.MPD_30FPS_LIMIT, False) def remote_friendly_search(self): diff --git a/resources/settings.xml b/resources/settings.xml index ec002b14b..c13e8d5af 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -273,17 +273,12 @@ - + 0 false - - true - false - 8 - 9 - + true From ef904ac0aeaa3d8fba36b19cd406221b7e180c8b Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 16 Jun 2023 00:20:45 +1000 Subject: [PATCH 09/13] Improve selection and display of video resolution - Correctly handles portrait videos - Correctly handles ultra wide AR videos - Code is messy due to backwards compatibility with quality settings --- .../kodion/impl/abstract_settings.py | 59 +++++++++---------- .../youtube/helper/video_info.py | 44 +++++++++----- 2 files changed, 57 insertions(+), 46 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py index 5dd184d7d..29b8a93d7 100644 --- a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py @@ -202,39 +202,36 @@ def get_play_count_min_percent(self): def use_playback_history(self): return self.get_bool(constants.setting.USE_PLAYBACK_HISTORY, False) - @staticmethod - def __get_mpd_quality_map(): - return { - 0: 240, - 1: 360, - 2: 480, - 3: 720, - 4: 1080, - 5: 1440, - 6: 2160, - 7: 4320, - 8: 'mp4', - 9: 'webm' - } - - def get_mpd_quality(self): - quality_map = self.__get_mpd_quality_map() - quality_enum = self.get_int(constants.setting.MPD_QUALITY_SELECTION, 8) - return quality_map.get(quality_enum, 'mp4') - - def mpd_video_qualities(self): + # Selections based on max width and min height at common (utra-)wide aspect ratios + # 8K and 4K at 32:9, 2K at 8:3, remainder at 22:9 (2.444...) + # MPD_QUALITY_SELECTION value + _QUALITY_SELECTIONS = ['mp4', # 8 (default) + 'webm', # 9 + {'width': 256, 'height': 105, 'label': '144p{0}{1}'}, # No setting + {'width': 426, 'height': 175, 'label': '240p{0}{1}'}, # 0 + {'width': 640, 'height': 263, 'label': '360p{0}{1}'}, # 1 + {'width': 854, 'height': 350, 'label': '480p{0}{1}'}, # 2 + {'width': 1280, 'height': 525, 'label': '720p{0} (HD){1}'}, # 3 + {'width': 1920, 'height': 787, 'label': '1080p{0} (FHD){1}'}, # 4 + {'width': 2560, 'height': 984, 'label': '1440p{0} (2K){1}'}, # 5 + {'width': 3840, 'height': 1080, 'label': '2160p{0} (4K){1}'}, # 6 + {'width': 7680, 'height': 3148, 'label': '4320p{0} (8K){1}'}, # 7 + {'width': 0, 'height': 0, 'label': '{2}p{0}{1}'}] # Unknown quality + + def get_mpd_video_qualities(self, list_all=False): if not self.use_dash_videos(): return [] - - quality = self.get_mpd_quality() - - if not isinstance(quality, int): - return quality - - quality_map = self.__get_mpd_quality_map() - qualities = sorted([x for x in list(quality_map.values()) - if isinstance(x, int) and x <= quality], reverse=True) - + if list_all: + # to be converted to selection index 2 + selected = 7 + else: + selected = self.get_int(constants.setting.MPD_QUALITY_SELECTION, 8) + if 8 <= selected <= 9: + # converted to selection index 0 or 1 + return self._QUALITY_SELECTIONS[selected - 8] + # converted to selection index starting from 2 + qualities = self._QUALITY_SELECTIONS[2:] + del qualities[2 + selected:-1] return qualities def mpd_30fps_limit(self): diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 5c56ecb70..1644fcbba 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1340,9 +1340,14 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): return None ia_capabilities = self._context.inputstream_adaptive_capabilities() - mpd_quality = self._context.get_settings().get_mpd_quality() - quality_type = isinstance(mpd_quality, str) and mpd_quality or '' - quality_height = isinstance(mpd_quality, int) and mpd_quality or 0 + qualities = self._context.get_settings().get_mpd_video_qualities() + if isinstance(qualities, str): + max_quality = None + selected_container = qualities + qualities = self._context.get_settings().get_mpd_video_qualities(list_all=True) + else: + max_quality = qualities[-2] + selected_container = None include_hdr = self._context.get_settings().include_hdr() and {'vp9.2', 'av1'} & ia_capabilities limit_30fps = self._context.get_settings().mpd_30fps_limit() @@ -1367,7 +1372,7 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): if not mime_type: continue - itag = str(stream_map.get('itag', '')) + itag = stream_map.get('itag') if not itag: continue @@ -1390,7 +1395,7 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): if codec: codec = codec.group(1) media_type, container = mime_type.split('/') - if ((quality_type and container != quality_type) + if ((selected_container and container != selected_container) or (mime_type == 'video/webm' and not {'vp9', 'vp9.2'} & ia_capabilities) or (mime_type == 'audio/webm' and not {'vorbis', 'opus'} & ia_capabilities) or (codec in {'av01', 'av1'} and 'av1' not in ia_capabilities) @@ -1447,24 +1452,33 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): # but that is too verbose language_code = '' height = stream_map.get('height') - if 0 < quality_height < height: - continue width = stream_map.get('width') - # map frame rates to a more common representation to lessen the chance of double refresh changes - # sometimes 30 fps is 30 fps, more commonly it is 29.97 fps (same for all mapped frame rates) + if height > width: + compare_width = height + compare_height = width + else: + compare_width = width + compare_height = height + if max_quality and compare_width > max_quality['width']: + continue fps = stream_map.get('fps', 0) if limit_30fps and fps > 30: continue + hdr = 'HDR' in stream_map.get('qualityLabel', '') + if hdr and not include_hdr: + continue + # map frame rates to a more common representation to lessen the chance of double refresh changes + # sometimes 30 fps is 30 fps, more commonly it is 29.97 fps (same for all mapped frame rates) if fps: frame_rate = '{0}/{1}'.format(fps * 1000, fps_scale_map.get(fps, 1000)) else: frame_rate = None - hdr = 'HDR' in stream_map.get('qualityLabel', '') - if hdr and not include_hdr: - continue - label = '{0}p{1}{2}'.format(height, - fps if fps > 30 else '', - ' HDR' if hdr else '') + for quality in qualities: + if compare_width <= quality['width'] and compare_height >= quality['height']: + break + label = quality['label'].format(fps if fps > 30 else '', + ' HDR' if hdr else '', + compare_height) key = mime_type role = sample_rate = channels = None From ade10b55e57cd55fccbbdf5a70cfc15aee7e4487 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 16 Jun 2023 16:57:39 +1000 Subject: [PATCH 10/13] Fix sorting of audio in stream selection dialog --- .../youtube/helper/video_info.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 1644fcbba..9968844df 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -451,71 +451,77 @@ class VideoInfo(object): 'video': {'height': 4320, 'encoding': 'av1'}}, # === Dash (audio only) '139': {'container': 'mp4', - 'sort': [48, 0], + 'sort': [0, 48 * 0.9], 'title': 'he-aac@48', 'dash/audio': True, 'audio': {'bitrate': 48, 'encoding': 'aac'}}, '140': {'container': 'mp4', - 'sort': [129, 0], + 'sort': [0, 128 * 0.9], 'title': 'aac-lc@128', 'dash/audio': True, 'audio': {'bitrate': 128, 'encoding': 'aac'}}, '141': {'container': 'mp4', - 'sort': [143, 0], + 'sort': [0, 256 * 0.9], 'title': 'aac-lc@256', 'dash/audio': True, 'audio': {'bitrate': 256, 'encoding': 'aac'}}, '256': {'container': 'mp4', + 'sort': [0, 192 * 0.9], 'title': 'he-aac@192', 'dash/audio': True, 'audio': {'bitrate': 192, 'encoding': 'aac'}}, '258': {'container': 'mp4', + 'sort': [0, 384 * 0.9], 'title': 'aac-lc@384', 'dash/audio': True, 'audio': {'bitrate': 384, 'encoding': 'aac'}}, '325': {'container': 'mp4', + 'sort': [0, 384 * 1.3], 'title': 'dtse@384', 'dash/audio': True, 'audio': {'bitrate': 384, 'encoding': 'dtse'}}, '327': {'container': 'mp4', + 'sort': [0, 256 * 0.9], 'title': 'aac-lc@256', 'dash/audio': True, 'audio': {'bitrate': 256, 'encoding': 'aac'}}, '328': {'container': 'mp4', + 'sort': [0, 384 * 1.2], 'title': 'ec-3@384', 'dash/audio': True, 'audio': {'bitrate': 384, 'encoding': 'ec-3'}}, '171': {'container': 'webm', - 'sort': [128, 0], + 'sort': [0, 128 * 0.75], 'title': 'vorbis@128', 'dash/audio': True, 'audio': {'bitrate': 128, 'encoding': 'vorbis'}}, '172': {'container': 'webm', - 'sort': [142, 0], + 'sort': [0, 192 * 0.75], 'title': 'vorbis@192', 'dash/audio': True, 'audio': {'bitrate': 192, 'encoding': 'vorbis'}}, '249': {'container': 'webm', - 'sort': [50, 0], + 'sort': [0, 50], 'title': 'opus@50', 'dash/audio': True, 'audio': {'bitrate': 50, 'encoding': 'opus'}}, '250': {'container': 'webm', - 'sort': [70, 0], + 'sort': [0, 70], 'title': 'opus@70', 'dash/audio': True, 'audio': {'bitrate': 70, 'encoding': 'opus'}}, '251': {'container': 'webm', - 'sort': [141, 0], + 'sort': [0, 160], 'title': 'opus@160', 'dash/audio': True, 'audio': {'bitrate': 160, 'encoding': 'opus'}}, '338': {'container': 'webm', - 'sort': [141, 0], + 'sort': [0, 480], 'title': 'opus@480', 'dash/audio': True, 'audio': {'bitrate': 480, 'encoding': 'opus'}}, '380': {'container': 'mp4', + 'sort': [0, 384 * 1.1], 'title': 'ac-3@384', 'dash/audio': True, 'audio': {'bitrate': 384, 'encoding': 'ac-3'}}, From 0f1f71ba61283257e49687fc160781575cfef295 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 16 Jun 2023 17:07:05 +1000 Subject: [PATCH 11/13] Update ISA capabilities map and simplify matching --- .../kodion/impl/xbmc/xbmc_context.py | 15 +++++++++++---- .../youtube_plugin/youtube/helper/video_info.py | 14 +++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py index 7a933521b..17915993d 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py @@ -312,13 +312,20 @@ def inputstream_adaptive_capabilities(self, capability=None): # - True to include capability regardless of version capability_map = { 'live': '2.0.12', - 'dts': '2.1.15', 'drm': '2.2.12', - 'vp9': '2.3.14', - 'vp9.2': '2.3.14', + # audio 'vorbis': '2.3.14', 'opus': '19.0.7', - 'av1': '20.3.0', + 'mp4a': True, + 'ac-3': '2.1.15', + 'ec-3': '2.1.15', + 'dts': '2.1.15', + # video + 'avc1': True, + 'av01': '20.3.0', + 'vp8': False, + 'vp9': '2.3.14', + 'vp9.2': '2.3.14', } if capability is None: diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 9968844df..ccd0cf265 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1345,7 +1345,6 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): self._context.log_debug('Failed to create directories: %s' % basepath) return None - ia_capabilities = self._context.inputstream_adaptive_capabilities() qualities = self._context.get_settings().get_mpd_video_qualities() if isinstance(qualities, str): max_quality = None @@ -1354,7 +1353,9 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): else: max_quality = qualities[-2] selected_container = None - include_hdr = self._context.get_settings().include_hdr() and {'vp9.2', 'av1'} & ia_capabilities + + ia_capabilities = self._context.inputstream_adaptive_capabilities() + include_hdr = self._context.get_settings().include_hdr() limit_30fps = self._context.get_settings().mpd_30fps_limit() ipaddress = self._context.get_settings().httpd_listen() @@ -1397,15 +1398,14 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): continue mime_type, codecs = unquote(mime_type).split('; ') - codec = re.match(r'codecs="([a-z0-9]+)', codecs) + codec = re.match(r'codecs="([a-z0-9]+([.\-][0-9](?="))?)', codecs) if codec: codec = codec.group(1) + if codec.startswith('dts'): + codec = 'dts' media_type, container = mime_type.split('/') if ((selected_container and container != selected_container) - or (mime_type == 'video/webm' and not {'vp9', 'vp9.2'} & ia_capabilities) - or (mime_type == 'audio/webm' and not {'vorbis', 'opus'} & ia_capabilities) - or (codec in {'av01', 'av1'} and 'av1' not in ia_capabilities) - or (codec.startswith('dts') and 'dts' not in ia_capabilities)): + or codec not in ia_capabilities): continue if media_type == 'audio': From 42e36bce298e128f4d6191fa4ec5bf2bb63852a7 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 16 Jun 2023 17:08:15 +1000 Subject: [PATCH 12/13] Improve labelling of more odd video resolutions --- resources/lib/youtube_plugin/youtube/helper/video_info.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index ccd0cf265..d8deb1fa1 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1353,6 +1353,7 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): else: max_quality = qualities[-2] selected_container = None + qualities = list(enumerate(qualities)) ia_capabilities = self._context.inputstream_adaptive_capabilities() include_hdr = self._context.get_settings().include_hdr() @@ -1479,8 +1480,10 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): frame_rate = '{0}/{1}'.format(fps * 1000, fps_scale_map.get(fps, 1000)) else: frame_rate = None - for quality in qualities: - if compare_width <= quality['width'] and compare_height >= quality['height']: + for idx, quality in qualities: + if compare_width <= quality['width']: + if compare_height < quality['height']: + quality = qualities[idx - 1][1] break label = quality['label'].format(fps if fps > 30 else '', ' HDR' if hdr else '', From a9b90baf60b776fb66bf3d4729165e21fa0db98c Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 16 Jun 2023 17:13:13 +1000 Subject: [PATCH 13/13] Improve MPD sorting and default quality selection - This won't fix incorrect ordering by ISA for manual selection - ISA performs it's own simple sorting that results in H264 being preferred over better AV1 streams --- .../youtube/helper/video_info.py | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index d8deb1fa1..0b3cc836f 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1369,6 +1369,21 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): 60: 1001 } + bitrate_bias_map = { + # video - order based on comparative compression ratio + 'av01': 1, + 'vp9': 0.75, + 'vp8': 0.55, + 'avc1': 0.5, + # audio - order based on preference + 'vorbis': 0.75, + 'mp4a': 0.9, + 'opus': 1, + 'ac-3': 1.1, + 'ec-3': 1.2, + 'dts': 1.3, + } + data = {} preferred_audio = { 'id': '', @@ -1498,6 +1513,9 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): url = self._process_url_params(url) url = url.replace("&", "&").replace('"', """).replace("<", "<").replace(">", ">") + bitrate = stream_map.get('bitrate', 0) + biased_bitrate = bitrate * bitrate_bias_map.get(codec, 1) + data[key][itag] = { 'mimeType': mime_type, 'baseUrl': url, @@ -1509,7 +1527,8 @@ def generate_mpd(self, adaptive_fmts, duration, license_url): 'width': width, 'height': height, 'label': label, - 'bitrate': stream_map.get('bitrate', 0), + 'bitrate': bitrate, + 'biasedBitrate': biased_bitrate, 'fps': fps, 'frameRate': frame_rate, 'hdr': hdr, @@ -1532,13 +1551,11 @@ def _stream_sort(stream): stream['height'], stream['fps'], stream['hdr'], - # Prefer lower bitrate for video streams - # Used to preference more advanced codecs - -stream['bitrate'], + stream['biasedBitrate'], ) if stream['mediaType'] == 'video' else ( stream['channels'], stream['sampleRate'], - stream['bitrate'], + stream['biasedBitrate'], ) data = {