From 767d20b02565f5b0c80a8780cc012284a91b889c Mon Sep 17 00:00:00 2001 From: CastagnaIT Date: Wed, 3 May 2023 13:50:19 +0200 Subject: [PATCH 1/2] [StringUtils] Add Trim, ToMap methods --- src/utils/StringUtils.cpp | 54 +++++++++++++++++++++++++++++++++++++++ src/utils/StringUtils.h | 18 +++++++++++++ 2 files changed, 72 insertions(+) diff --git a/src/utils/StringUtils.cpp b/src/utils/StringUtils.cpp index b81239596..759ea1571 100644 --- a/src/utils/StringUtils.cpp +++ b/src/utils/StringUtils.cpp @@ -11,6 +11,7 @@ #include "kodi/tools/StringUtils.h" #include +#include // isspace #include // from_chars #include #include // strstr @@ -255,3 +256,56 @@ std::string UTILS::STRING::ToLower(std::string str) StringUtils::ToLower(str); return str; } + +std::map UTILS::STRING::ToMap(std::string_view str, + const char delimiter, + const char separator) +{ + std::map mapped; + + size_t keyPos = 0; + size_t keyEnd; + size_t valPos; + size_t valEnd; + + while ((keyEnd = str.find(delimiter, keyPos)) != std::string::npos) + { + valPos = str.find_first_not_of(delimiter, keyEnd); + if (valPos == std::string::npos) + break; + + valEnd = str.find(separator, valPos); + mapped.emplace(str.substr(keyPos, keyEnd - keyPos), str.substr(valPos, valEnd - valPos)); + + keyPos = valEnd; + if (keyPos != std::string::npos) + ++keyPos; + } + + return mapped; +} + +std::string_view UTILS::STRING::Trim(std::string_view str) +{ + auto left = str.begin(); + while (left != str.end()) + { + if (!std::isspace(*left)) + break; + + left++; + } + + if (left == str.end()) + return {}; + + auto right = str.end() - 1; + while (right > left && std::isspace(*right)) + { + right--; + } + + //! @todo: when we will switch to C++20 replace return code with: + //! return {left, std::distance(left, right) + 1}; + return str.substr(left - str.begin(), std::distance(left, right) + 1); +} diff --git a/src/utils/StringUtils.h b/src/utils/StringUtils.h index de6a6f182..1cb78b43f 100644 --- a/src/utils/StringUtils.h +++ b/src/utils/StringUtils.h @@ -9,6 +9,7 @@ #pragma once #include +#include #include #include #include @@ -164,5 +165,22 @@ bool GetLine(std::stringstream& ss, std::string& line); */ std::string ToLower(std::string str); +/*! + * \brief Convert a string to a map. + * \param str The string to convert + * \param delimiter The character separating the key from the value + * \param separator The character separating more key/value pairs + * \return The mapped string. + */ +std::map ToMap(std::string_view str, + const char delimiter, + const char separator); + +/*! + * \brief Trim a string with remove of not wanted spaces at begin and end of string. + * \return The changed string. + */ +std::string_view Trim(std::string_view str); + } // namespace STRING } // namespace UTILS From aa6d50051617681cbfc5b7771c76f6604580dc1b Mon Sep 17 00:00:00 2001 From: CastagnaIT Date: Wed, 7 Jun 2023 11:06:33 +0200 Subject: [PATCH 2/2] [PropertiesUtils] Add streams_config property --- inputstream.adaptive/addon.xml.in | 2 +- src/Session.cpp | 7 +- src/common/AdaptationSet.cpp | 21 +++++ src/common/AdaptationSet.h | 6 ++ src/common/AdaptiveTree.cpp | 146 ++++++++++++++++++++++++++++++ src/common/AdaptiveTree.h | 6 ++ src/parser/DASHTree.cpp | 4 +- src/utils/PropertiesUtils.cpp | 28 +++++- src/utils/PropertiesUtils.h | 16 +++- 9 files changed, 225 insertions(+), 11 deletions(-) diff --git a/inputstream.adaptive/addon.xml.in b/inputstream.adaptive/addon.xml.in index 4960efbe9..1fb1edc56 100644 --- a/inputstream.adaptive/addon.xml.in +++ b/inputstream.adaptive/addon.xml.in @@ -10,7 +10,7 @@ name="adaptive" extension="" tags="true" - listitemprops="license_type|license_key|license_data|license_flags|manifest_type|server_certificate|manifest_update_parameter|manifest_params|manifest_headers|stream_params|stream_headers|original_audio_language|play_timeshift_buffer|pre_init_data|stream_selection_type|chooser_bandwidth_max|chooser_resolution_max|chooser_resolution_secure_max|live_delay" + listitemprops="license_type|license_key|license_data|license_flags|manifest_type|server_certificate|manifest_update_parameter|manifest_params|manifest_headers|stream_params|stream_headers|original_audio_language|play_timeshift_buffer|pre_init_data|stream_selection_type|chooser_bandwidth_max|chooser_resolution_max|chooser_resolution_secure_max|live_delay|streams_config" library_@PLATFORM@="@LIBRARY_FILENAME@"/> @PLATFORM@ diff --git a/src/Session.cpp b/src/Session.cpp index 8db7d3059..3d8b5084f 100644 --- a/src/Session.cpp +++ b/src/Session.cpp @@ -798,13 +798,10 @@ void CSession::AddStream(PLAYLIST::CAdaptationSet* adp, stream.m_info.SetStreamType(INPUTSTREAM_TYPE_AUDIO); if (adp->IsImpaired()) flags |= INPUTSTREAM_FLAG_VISUAL_IMPAIRED; + if (adp->IsOriginal()) + flags |= INPUTSTREAM_FLAG_ORIGINAL; if (adp->IsDefault()) flags |= INPUTSTREAM_FLAG_DEFAULT; - if (adp->IsOriginal() || (!m_kodiProps.m_audioLanguageOrig.empty() && - adp->GetLanguage() == m_kodiProps.m_audioLanguageOrig)) - { - flags |= INPUTSTREAM_FLAG_ORIGINAL; - } break; } case StreamType::SUBTITLE: diff --git a/src/common/AdaptationSet.cpp b/src/common/AdaptationSet.cpp index e96b1be39..ceb3c3515 100644 --- a/src/common/AdaptationSet.cpp +++ b/src/common/AdaptationSet.cpp @@ -157,3 +157,24 @@ bool PLAYLIST::CAdaptationSet::Compare(const std::unique_ptr& le return false; } + +std::vector>::const_iterator PLAYLIST::CAdaptationSet:: + FindAudioAdpSet(const std::vector>& adpSets, + const std::string langCode, + bool isPreferStereo, + bool filterImpaired) +{ + for (auto& itAdpSet = adpSets.cbegin(); itAdpSet != adpSets.cend(); itAdpSet++) + { + auto adpSet = itAdpSet->get(); + if (adpSet->GetStreamType() == StreamType::AUDIO && + STRING::CompareNoCase(adpSet->GetLanguage(), langCode) && + (isPreferStereo ? adpSet->GetRepresentations()[0]->GetAudioChannels() <= 2 + : adpSet->GetRepresentations()[0]->GetAudioChannels() > 2) && + adpSet->IsImpaired() == filterImpaired) + { + return itAdpSet; + } + } + return adpSets.end(); +} diff --git a/src/common/AdaptationSet.h b/src/common/AdaptationSet.h index da40b42fc..d2500590d 100644 --- a/src/common/AdaptationSet.h +++ b/src/common/AdaptationSet.h @@ -109,6 +109,12 @@ class ATTR_DLL_LOCAL CAdaptationSet : public CCommonSegAttribs, public CCommonAt static bool Compare(const std::unique_ptr& left, const std::unique_ptr& right); + static std::vector>::const_iterator FindAudioAdpSet( + const std::vector>& adpSets, + const std::string langCode, + bool isPreferStereo, + bool filterImpaired = false); + protected: std::vector> m_representations; diff --git a/src/common/AdaptiveTree.cpp b/src/common/AdaptiveTree.cpp index 6104ba838..0ab44107e 100644 --- a/src/common/AdaptiveTree.cpp +++ b/src/common/AdaptiveTree.cpp @@ -68,6 +68,8 @@ namespace adaptive void AdaptiveTree::PostOpen(const UTILS::PROPERTIES::KodiProperties& kodiProps) { + FixStreamsFlags(kodiProps); + // A manifest can provide live delay value, if not so we use our default // value of 16 secs, this is needed to ensure an appropriate playback, // an add-on can override the delay to try fix edge use cases @@ -374,4 +376,148 @@ namespace adaptive m_cvWait.notify_all(); } + void AdaptiveTree::FixStreamsFlags(const UTILS::PROPERTIES::KodiProperties& kodiProps) + { + // Add-ons can override subtitles "default" flag to streams + if (!kodiProps.m_subtitleLangDefault.empty()) + { + for (auto& period : m_periods) + { + for (auto& adpSet : period->GetAdaptationSets()) + { + if (adpSet->GetStreamType() == StreamType::SUBTITLE) + { + adpSet->SetIsDefault( + STRING::CompareNoCase(adpSet->GetLanguage(), kodiProps.m_subtitleLangDefault)); + } + } + } + } + + // Add-ons can override audio "original" flag to streams + if (!kodiProps.m_audioLangOriginal.empty()) + { + for (auto& period : m_periods) + { + for (auto& adpSet : period->GetAdaptationSets()) + { + if (adpSet->GetStreamType() == StreamType::AUDIO) + { + adpSet->SetIsOriginal( + STRING::CompareNoCase(adpSet->GetLanguage(), kodiProps.m_audioLangOriginal)); + } + } + } + } + + // Audio streams "default" flag customization / workaround + + // Manifest of video services dont always set appropriately the default stream flag and also + // the manifest "default" stream flag dont have always the same meaning of Kodi track "default" flag, + // this can lead to wrong audio track selected when playback start. + // A good example is when "Media default" Kodi audio setting is set, where kodi expects just + // a single track with the default flag. + // Another problem is when video services provide multiple audio streams with same language code + // but differents channels, most of the times we can have 1 stereo and 1 multichannels + // stream with same language code, rarely there are multi-codecs with same channels, + // but we simplify by ignoring codec types. + + // To allow Kodi VP to do a better track auto-selection we need: + // - Set default flag to a single track only + // - Set default flag to stereo or multichannels track, not both + // to do this its needed that an addon specify what to do because C++ interface dont provide + // access to kodi language settings where python can do it, then we cant automatize it. + const std::string langCodeDef = kodiProps.m_audioLangDefault; + const std::string langCodeOrig = kodiProps.m_audioLangOriginal; + + if (!langCodeDef.empty() || !langCodeOrig.empty()) + { + bool isDefaultStereo = kodiProps.m_audioPrefStereo; // add-on based setting + + for (auto& period : m_periods) + { + auto& adpSets = period->GetAdaptationSets(); + auto itAudioStream = adpSets.cend(); + + // Try give priority to "impaired" streams + if (kodiProps.m_audioPrefType == "impaired") + { + if (isDefaultStereo) + { + itAudioStream = CAdaptationSet::FindAudioAdpSet(adpSets, langCodeDef, true, true); + if (itAudioStream == adpSets.cend()) // No stereo stream, find multichannels + itAudioStream = CAdaptationSet::FindAudioAdpSet(adpSets, langCodeDef, false, true); + } + else + { + itAudioStream = CAdaptationSet::FindAudioAdpSet(adpSets, langCodeDef, false, true); + if (itAudioStream == adpSets.cend()) // No multichannels stream, find stereo + itAudioStream = CAdaptationSet::FindAudioAdpSet(adpSets, langCodeDef, true, true); + } + + // No stream found, try find a "impaired" stream with the "original" language code + if (itAudioStream == adpSets.cend() && !langCodeOrig.empty()) + { + if (isDefaultStereo) + { + itAudioStream = CAdaptationSet::FindAudioAdpSet(adpSets, langCodeOrig, true, true); + if (itAudioStream == adpSets.cend()) // No stereo stream, find multichannels + itAudioStream = CAdaptationSet::FindAudioAdpSet(adpSets, langCodeOrig, false, true); + } + else + { + itAudioStream = CAdaptationSet::FindAudioAdpSet(adpSets, langCodeOrig, false, true); + if (itAudioStream == adpSets.cend()) // No multichannels stream, find stereo + itAudioStream = CAdaptationSet::FindAudioAdpSet(adpSets, langCodeOrig, true, true); + } + } + } + + // Try find a stream with specified lang code + if (kodiProps.m_audioPrefType != "original" && itAudioStream == adpSets.cend() && + !langCodeDef.empty()) + { + if (isDefaultStereo) + { + itAudioStream = CAdaptationSet::FindAudioAdpSet(adpSets, langCodeDef, true); + if (itAudioStream == adpSets.cend()) // No stereo stream, find multichannels + itAudioStream = CAdaptationSet::FindAudioAdpSet(adpSets, langCodeDef, false); + } + else + { + itAudioStream = CAdaptationSet::FindAudioAdpSet(adpSets, langCodeDef, false); + if (itAudioStream == adpSets.cend()) // No multichannels stream, find stereo + itAudioStream = CAdaptationSet::FindAudioAdpSet(adpSets, langCodeDef, true); + } + } + + // No stream found, try find a stream with the "original" language code + if (itAudioStream == adpSets.cend() && !langCodeOrig.empty()) + { + if (isDefaultStereo) + { + itAudioStream = CAdaptationSet::FindAudioAdpSet(adpSets, langCodeOrig, true); + if (itAudioStream == adpSets.cend()) // No stereo stream, find multichannels + itAudioStream = CAdaptationSet::FindAudioAdpSet(adpSets, langCodeOrig, false); + } + else + { + itAudioStream = CAdaptationSet::FindAudioAdpSet(adpSets, langCodeOrig, false); + if (itAudioStream == adpSets.cend()) // No multichannels stream, find stereo + itAudioStream = CAdaptationSet::FindAudioAdpSet(adpSets, langCodeOrig, true); + } + } + + // Update "default" flags + if (itAudioStream != adpSets.cend()) + { + for (auto& adpSet : adpSets) + { + adpSet->SetIsDefault(adpSet.get() == itAudioStream->get()); + } + } + } + } + } + } // namespace adaptive diff --git a/src/common/AdaptiveTree.h b/src/common/AdaptiveTree.h index 565719f1f..2f7cc364b 100644 --- a/src/common/AdaptiveTree.h +++ b/src/common/AdaptiveTree.h @@ -254,6 +254,12 @@ class ATTR_DLL_LOCAL AdaptiveTree const std::string& data, std::string_view info); + /*! + * \brief Apply fixes and overrides to audio/subtitles stream flags. + * \param kodiProps The Kodi properties + */ + void FixStreamsFlags(const UTILS::PROPERTIES::KodiProperties& kodiProps); + void SortTree(); // Live segment update section diff --git a/src/parser/DASHTree.cpp b/src/parser/DASHTree.cpp index 3e00864d6..6121abfb9 100644 --- a/src/parser/DASHTree.cpp +++ b/src/parser/DASHTree.cpp @@ -376,6 +376,8 @@ void adaptive::CDashTree::ParseTagAdaptationSet(pugi::xml_node nodeAdp, PLAYLIST adpSet->SetIsForced(true); else if (value == "main") adpSet->SetIsDefault(true); + else if (value == "caption" || value == "alternate" || value == "commentary") + adpSet->SetIsImpaired(true); } } @@ -388,7 +390,7 @@ void adaptive::CDashTree::ParseTagAdaptationSet(pugi::xml_node nodeAdp, PLAYLIST if (schemeIdUri == "urn:mpeg:dash:role:2011") { - if (value == "caption") + if (STRING::StartsWith(value, "caption")) // caption or captions adpSet->SetIsImpaired(true); } } diff --git a/src/utils/PropertiesUtils.cpp b/src/utils/PropertiesUtils.cpp index de3c1f351..5cac82bb9 100644 --- a/src/utils/PropertiesUtils.cpp +++ b/src/utils/PropertiesUtils.cpp @@ -34,7 +34,9 @@ constexpr std::string_view PROP_MANIFEST_HEADERS = "inputstream.adaptive.manifes constexpr std::string_view PROP_STREAM_PARAMS = "inputstream.adaptive.stream_params"; constexpr std::string_view PROP_STREAM_HEADERS = "inputstream.adaptive.stream_headers"; -constexpr std::string_view PROP_AUDIO_LANG_ORIG = "inputstream.adaptive.original_audio_language"; +constexpr std::string_view PROP_AUDIO_LANG_ORIG = "inputstream.adaptive.original_audio_language"; //! @todo: deprecated, to be removed on next Kodi release +constexpr std::string_view PROP_STREAMS_CONFIG = "inputstream.adaptive.streams_config"; + constexpr std::string_view PROP_PLAY_TIMESHIFT_BUFFER = "inputstream.adaptive.play_timeshift_buffer"; constexpr std::string_view PROP_LIVE_DELAY = "inputstream.adaptive.live_delay"; constexpr std::string_view PROP_PRE_INIT_DATA = "inputstream.adaptive.pre_init_data"; @@ -121,9 +123,29 @@ KodiProperties UTILS::PROPERTIES::ParseKodiProperties( { ParseHeaderString(props.m_streamHeaders, prop.second); } - else if (prop.first == PROP_AUDIO_LANG_ORIG) + else if (prop.first == PROP_AUDIO_LANG_ORIG) //! @todo: deprecated, to be removed on next Kodi release + { + LOG::Log(LOGWARNING, + "Warning \"inputstream.adaptive.original_audio_language\" property is deprecated " + "has been replaced by \"inputstream.adaptive.stream_audio_cfg\". " + "Please read Wiki \"Integration\" page to learn more about the new properties."); + props.m_audioLangOriginal = prop.second; + } + else if (prop.first == PROP_STREAMS_CONFIG) { - props.m_audioLanguageOrig = prop.second; + auto values = STRING::ToMap(prop.second, '=', ';'); + + if (STRING::KeyExists(values, "audio_langcode_default")) + props.m_audioLangDefault = STRING::Trim(values["audio_langcode_default"]); + if (STRING::KeyExists(values, "audio_langcode_original")) + props.m_audioLangOriginal = STRING::Trim(values["audio_langcode_original"]); + if (STRING::KeyExists(values, "audio_prefer_stereo")) + props.m_audioPrefStereo = values["audio_prefer_stereo"] == "true"; + if (STRING::KeyExists(values, "audio_prefer_type")) + props.m_audioPrefType = STRING::Trim(values["audio_prefer_type"]); + + if (STRING::KeyExists(values, "subtitles_langcode_default")) + props.m_subtitleLangDefault = STRING::Trim(values["subtitles_langcode_default"]); } else if (prop.first == PROP_PLAY_TIMESHIFT_BUFFER) { diff --git a/src/utils/PropertiesUtils.h b/src/utils/PropertiesUtils.h index 9dbea0fc9..d6f45d588 100644 --- a/src/utils/PropertiesUtils.h +++ b/src/utils/PropertiesUtils.h @@ -53,7 +53,21 @@ struct KodiProperties std::string m_streamParams; // HTTP headers used to download streams std::map m_streamHeaders; - std::string m_audioLanguageOrig; + + // Defines what type of audio tracks should be preferred for the "default" flag, + // accepted values are: original, impaired, or empty string. + // When empty: it try to set the flag to a regular language track or fallback to original language + std::string m_audioPrefType; + // Defines if stereo audio tracks are preferred over multichannels one, + // it depends from m_audioLangDefault + bool m_audioPrefStereo{false}; + // Force audio streams with the specified language code to have the "default" flag + std::string m_audioLangDefault; + // Force audio streams with the specified language code to have the "original" flag + std::string m_audioLangOriginal; + // Force subtitle streams with the specified language code to have the "default" flag + std::string m_subtitleLangDefault; + bool m_playTimeshiftBuffer{false}; // Set a custom delay from live edge in seconds uint64_t m_liveDelay{0};