Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 71 additions & 14 deletions core/playback_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,39 @@ def should_filter_by_max_width(width, maxwidth):
return width is not None and width > maxwidth


def should_allow_native_hls_without_isa(format_info, manifest_type):
if manifest_type != 'hls':
return False

protocol = (format_info.get('protocol') or '').lower()
if not protocol.startswith('m3u'):
return False

vcodec = (format_info.get('vcodec') or '').lower()
acodec = (format_info.get('acodec') or '').lower()
unknown_values = ('', 'none', 'unknown')
is_audio_only = vcodec in unknown_values and acodec not in unknown_values
is_muxed_av = vcodec not in unknown_values and acodec not in unknown_values
return is_audio_only or is_muxed_av


def should_skip_audio_only_hls_native_opus(format_info, manifest_type, disable_opus_for_audio_only_hls_native):
if not disable_opus_for_audio_only_hls_native:
return False
if manifest_type != 'hls':
return False

protocol = (format_info.get('protocol') or '').lower()
if not protocol.startswith('m3u'):
return False

vcodec = (format_info.get('vcodec') or '').lower()
acodec = (format_info.get('acodec') or '').lower()
unknown_values = ('', 'none', 'unknown')
is_audio_only = vcodec in unknown_values and acodec not in unknown_values
return is_audio_only and acodec == 'opus'


def should_try_dash_builder(usedashbuilder, have_video, dash_video, have_audio, dash_audio, current_format, mpd_supported):
return (
usedashbuilder
Expand Down Expand Up @@ -101,7 +134,15 @@ def resolve_result_fallback_candidate(result_url, manifest_supported, headers):
}


def evaluate_raw_format_candidate(format_info, have_video, have_audio, maxwidth, manifest_type, manifest_supported):
def evaluate_raw_format_candidate(
format_info,
have_video,
have_audio,
maxwidth,
manifest_type,
manifest_supported,
disable_opus_for_audio_only_hls_native=False,
):
if 'url' not in format_info:
return {'decision': 'skip'}

Expand All @@ -113,7 +154,15 @@ def evaluate_raw_format_candidate(format_info, have_video, have_audio, maxwidth,
):
return {'decision': 'skip'}

if manifest_type is not None and not manifest_supported:
if should_skip_audio_only_hls_native_opus(
format_info,
manifest_type,
disable_opus_for_audio_only_hls_native,
):
return {'decision': 'skip'}

native_hls_without_isa = should_allow_native_hls_without_isa(format_info, manifest_type)
if manifest_type is not None and not manifest_supported and not native_hls_without_isa:
return {'decision': 'skip'}

width = format_info.get('width', 0)
Expand All @@ -123,7 +172,8 @@ def evaluate_raw_format_candidate(format_info, have_video, have_audio, maxwidth,
return {
'decision': 'select',
'url': format_info['url'],
'isa': manifest_supported,
# For muxed HLS variants this can be more reliable than ISA on some Kodi setups.
'isa': False if native_hls_without_isa else manifest_supported,
'headers': format_info.get('http_headers'),
}

Expand Down Expand Up @@ -300,6 +350,8 @@ def select_playback_source(
maxwidth,
isa_supports,
dashbuilder=None,
preferred_format_url=None,
disable_opus_for_audio_only_hls_native=False,
):
dash_manifest_factory = None
dash_start_httpd = None
Expand All @@ -313,7 +365,7 @@ def select_playback_source(
isa_supports(guess_manifest_type(result, manifest_url)) if manifest_url is not None else False,
result.get('http_headers'),
)
if original_manifest_candidate is not None:
if original_manifest_candidate is not None and preferred_format_url is None:
original_manifest_candidate['source'] = 'original_manifest'
return original_manifest_candidate

Expand All @@ -327,16 +379,20 @@ def select_playback_source(
if should_skip_manifest_candidate(have_video, vcodec, acodec):
continue

manifest_url = format_info.get('manifest_url') if usemanifest else None
format_manifest_candidate = resolve_manifest_candidate(
manifest_url,
isa_supports(guess_manifest_type(format_info, manifest_url)) if manifest_url is not None else False,
format_info.get('http_headers'),
)
if format_manifest_candidate is not None:
format_manifest_candidate['source'] = 'format_manifest'
format_manifest_candidate['format_label'] = format_info.get('format', "")
return format_manifest_candidate
if preferred_format_url is not None and format_info.get('url') != preferred_format_url:
continue

if preferred_format_url is None:
manifest_url = format_info.get('manifest_url') if usemanifest else None
format_manifest_candidate = resolve_manifest_candidate(
manifest_url,
isa_supports(guess_manifest_type(format_info, manifest_url)) if manifest_url is not None else False,
format_info.get('http_headers'),
)
if format_manifest_candidate is not None:
format_manifest_candidate['source'] = 'format_manifest'
format_manifest_candidate['format_label'] = format_info.get('format', "")
return format_manifest_candidate

if should_try_dash_builder(
usedashbuilder,
Expand Down Expand Up @@ -377,6 +433,7 @@ def select_playback_source(
maxwidth,
manifest_type,
isa_supports(manifest_type),
disable_opus_for_audio_only_hls_native,
)
if raw_candidate['decision'] == 'skip':
continue
Expand Down
85 changes: 82 additions & 3 deletions core/runtime/playback.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,67 @@ def _resolve_downloaded_file_path(result):
return None


def _format_stream_option(format_info):
format_label = format_info.get("format") or format_info.get("format_id") or "unknown"
protocol = format_info.get("protocol") or "?"
vcodec = format_info.get("vcodec") or "?"
acodec = format_info.get("acodec") or "?"
width = format_info.get("width")
height = format_info.get("height")
resolution = "{}x{}".format(width, height) if width and height else "?"
return "{} | {} | {} / {} | {}".format(format_label, protocol, vcodec, acodec, resolution)


def _normalize_codec(codec):
value = (codec or "").strip().lower()
if value in ("", "none", "unknown", "null"):
return "none"
return value


def _infer_selected_stream_kind(result, selected_url):
for format_info in reversed(result.get("formats", [])):
if format_info.get("url") != selected_url:
continue

vcodec = _normalize_codec(format_info.get("vcodec"))
acodec = _normalize_codec(format_info.get("acodec"))
if vcodec == "none" and acodec != "none":
return "audio"
return "video"

return "video"


def _prompt_preferred_stream_url(result):
formats = result.get("formats", [])
entries = []
seen_urls = set()
for format_info in reversed(formats):
stream_url = format_info.get("url")
if not stream_url or stream_url in seen_urls:
continue
seen_urls.add(stream_url)
entries.append((stream_url, _format_stream_option(format_info)))

if not entries:
return None

labels = ["Automatic selection"] + [label for _, label in entries]
selected_index = xbmcgui.Dialog().select("Select stream", labels)
if selected_index <= 0:
return None
return entries[selected_index - 1][0]


def create_list_item_from_video(
result,
ydl_opts,
usemanifest,
usedashbuilder,
maxwidth,
askstream,
disable_opus_for_audio_only_hls_native,
isa_supports,
youtube_dl_cls,
log,
Expand All @@ -58,15 +113,30 @@ def resolve_fresh_result():

selection_result = dict(result)
selection_result["resolve_fresh_result"] = resolve_fresh_result
preferred_stream_url = _prompt_preferred_stream_url(selection_result) if askstream else None
selected_source = select_playback_source(
selection_result,
usemanifest,
usedashbuilder,
maxwidth,
isa_supports,
dash_builder,
preferred_stream_url,
disable_opus_for_audio_only_hls_native,
)

if selected_source is None and preferred_stream_url is not None:
log("Selected stream is not playable, falling back to automatic selection", xbmc.LOGWARNING)
selected_source = select_playback_source(
selection_result,
usemanifest,
usedashbuilder,
maxwidth,
isa_supports,
dash_builder,
disable_opus_for_audio_only_hls_native=disable_opus_for_audio_only_hls_native,
)

if selected_source is not None:
for message in selection_log_messages(selected_source):
log(message)
Expand Down Expand Up @@ -97,9 +167,14 @@ def resolve_fresh_result():

log("creating list item for url {}".format(url))
list_item = xbmcgui.ListItem(result["title"], path=url)
video_info = list_item.getVideoInfoTag()
video_info.setTitle(result["title"])
video_info.setPlot(result.get("description", None))
stream_kind = _infer_selected_stream_kind(result, url)
if stream_kind == "audio":
music_info = list_item.getMusicInfoTag()
music_info.setTitle(result["title"])
else:
video_info = list_item.getVideoInfoTag()
video_info.setTitle(result["title"])
video_info.setPlot(result.get("description", None))
if result.get("thumbnail", None) is not None:
list_item.setArt({"thumb": result["thumbnail"]})

Expand Down Expand Up @@ -161,6 +236,8 @@ def play_playlist_result(
usemanifest,
usedashbuilder,
maxwidth,
askstream,
disable_opus_for_audio_only_hls_native,
isa_supports,
youtube_dl_cls,
log,
Expand All @@ -185,6 +262,8 @@ def extract_starting_entry(url, _download):
usemanifest,
usedashbuilder,
maxwidth,
askstream,
disable_opus_for_audio_only_hls_native,
isa_supports,
youtube_dl_cls,
log,
Expand Down
8 changes: 8 additions & 0 deletions resources/language/resource.language.en_gb/strings.po
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ msgctxt "#33040"
msgid "DASH MPD server idle timeout (seconds)"
msgstr ""

msgctxt "#33045"
msgid "Ask which stream to play"
msgstr ""

msgctxt "#33046"
msgid "Audio-only HLS: disable Opus for native m3u streams"
msgstr ""

msgctxt "#33030"
msgid "Maximum resolution"
msgstr ""
Expand Down
10 changes: 10 additions & 0 deletions resources/settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@
<default>true</default>
<control type="toggle"/>
</setting>
<setting type="boolean" id="askstream" label="33045">
<level>0</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting type="boolean" id="audio_only_hls_disable_opus_native" label="33046">
<level>0</level>
<default>true</default>
<control type="toggle"/>
</setting>
<setting type="string" id="dash_httpd_idle_timeout" label="33040" parent="usedashbuilder">
<level>0</level>
<default>120</default>
Expand Down
8 changes: 8 additions & 0 deletions service.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ def handle_queue_action(paramstring):

usemanifest = xbmcplugin.getSetting(__handle__,"usemanifest") == 'true'
usedashbuilder = xbmcplugin.getSetting(__handle__,"usedashbuilder") == 'true'
askstream = xbmcplugin.getSetting(__handle__,"askstream") == 'true'
disable_opus_for_audio_only_hls_native = (
xbmcplugin.getSetting(__handle__, "audio_only_hls_disable_opus_native") == 'true'
)
dash_httpd_idle_timeout_seconds = resolve_dash_httpd_idle_timeout(__handle__, xbmcplugin.getSetting)
dash_builder.DASH_HTTPD_IDLE_TIMEOUT_SECONDS = dash_httpd_idle_timeout_seconds
log("DASH MPD server idle timeout: {}s".format(dash_httpd_idle_timeout_seconds))
Expand Down Expand Up @@ -223,6 +227,8 @@ def handle_queue_action(paramstring):
usemanifest,
usedashbuilder,
maxwidth,
askstream,
disable_opus_for_audio_only_hls_native,
isa_supports,
YoutubeDL,
log,
Expand All @@ -238,6 +244,8 @@ def handle_queue_action(paramstring):
usemanifest,
usedashbuilder,
maxwidth,
askstream,
disable_opus_for_audio_only_hls_native,
isa_supports,
YoutubeDL,
log,
Expand Down
Loading
Loading