| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "media/formats/hls/tags.h" |
| |
| #include <cstddef> |
| #include <type_traits> |
| #include <utility> |
| |
| #include "base/logging.h" |
| #include "base/notreached.h" |
| #include "base/time/time.h" |
| #include "media/base/mime_util.h" |
| #include "media/formats/hls/items.h" |
| #include "media/formats/hls/parse_status.h" |
| #include "media/formats/hls/variable_dictionary.h" |
| |
| namespace media::hls { |
| |
| namespace { |
| |
| template <typename T> |
| ParseStatus::Or<T> ParseEmptyTag(TagItem tag) { |
| DCHECK(tag.GetName() == ToTagName(T::kName)); |
| if (tag.GetContent().has_value()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| return T{}; |
| } |
| |
| template <typename T> |
| ParseStatus::Or<T> ParseDecimalIntegerTag(TagItem tag, |
| types::DecimalInteger T::*field) { |
| DCHECK(tag.GetName() == ToTagName(T::kName)); |
| if (!tag.GetContent().has_value()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| auto value = |
| types::ParseDecimalInteger(tag.GetContent()->SkipVariableSubstitution()); |
| if (!value.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(value).error()); |
| } |
| |
| T out; |
| out.*field = std::move(value).value(); |
| return out; |
| } |
| |
| template <typename T> |
| ParseStatus::Or<T> ParseISO8601DateTimeTag(TagItem tag, base::Time T::*field) { |
| CHECK(tag.GetName() == ToTagName(T::kName)); |
| if (!tag.GetContent().has_value()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| const auto content = tag.GetContent()->SkipVariableSubstitution().Str(); |
| std::string content_nullterm = std::string(content); |
| T out; |
| base::Time time; |
| if (base::Time::FromString(content_nullterm.c_str(), &time)) { |
| out.*field = time; |
| } else { |
| return ParseStatusCode::kMalformedTag; |
| } |
| return out; |
| } |
| |
| // Attributes expected in `EXT-X-DEFINE` tag contents. |
| // These must remain sorted alphabetically. |
| enum class XDefineTagAttribute { |
| kImport, |
| kName, |
| kValue, |
| kMaxValue = kValue, |
| }; |
| |
| constexpr std::string_view GetAttributeName(XDefineTagAttribute attribute) { |
| switch (attribute) { |
| case XDefineTagAttribute::kImport: |
| return "IMPORT"; |
| case XDefineTagAttribute::kName: |
| return "NAME"; |
| case XDefineTagAttribute::kValue: |
| return "VALUE"; |
| } |
| |
| NOTREACHED_NORETURN(); |
| } |
| |
| // Attributes expected in `EXT-X-MEDIA` tag contents. |
| // These must remain sorted alphabetically. |
| enum class XMediaTagAttribute { |
| kAssocLanguage, |
| kAutoselect, |
| kChannels, |
| kCharacteristics, |
| kDefault, |
| kForced, |
| kGroupId, |
| kInstreamId, |
| kLanguage, |
| kName, |
| kStableRenditionId, |
| kType, |
| kUri, |
| kMaxValue = kUri, |
| }; |
| |
| constexpr std::string_view GetAttributeName(XMediaTagAttribute attribute) { |
| switch (attribute) { |
| case XMediaTagAttribute::kAssocLanguage: |
| return "ASSOC-LANGUAGE"; |
| case XMediaTagAttribute::kAutoselect: |
| return "AUTOSELECT"; |
| case XMediaTagAttribute::kChannels: |
| return "CHANNELS"; |
| case XMediaTagAttribute::kCharacteristics: |
| return "CHARACTERISTICS"; |
| case XMediaTagAttribute::kDefault: |
| return "DEFAULT"; |
| case XMediaTagAttribute::kForced: |
| return "FORCED"; |
| case XMediaTagAttribute::kGroupId: |
| return "GROUP-ID"; |
| case XMediaTagAttribute::kInstreamId: |
| return "INSTREAM-ID"; |
| case XMediaTagAttribute::kLanguage: |
| return "LANGUAGE"; |
| case XMediaTagAttribute::kName: |
| return "NAME"; |
| case XMediaTagAttribute::kStableRenditionId: |
| return "STABLE-RENDITION-ID"; |
| case XMediaTagAttribute::kType: |
| return "TYPE"; |
| case XMediaTagAttribute::kUri: |
| return "URI"; |
| } |
| |
| NOTREACHED_NORETURN(); |
| } |
| |
| // Attributes expected in `EXT-X-STREAM-INF` tag contents. |
| // These must remain sorted alphabetically. |
| enum class XStreamInfTagAttribute { |
| kAudio, |
| kAverageBandwidth, |
| kBandwidth, |
| kCodecs, |
| kFrameRate, |
| kProgramId, // Ignored for backwards compatibility |
| kResolution, |
| kScore, |
| kVideo, |
| kMaxValue = kVideo, |
| }; |
| |
| constexpr std::string_view GetAttributeName(XStreamInfTagAttribute attribute) { |
| switch (attribute) { |
| case XStreamInfTagAttribute::kAudio: |
| return "AUDIO"; |
| case XStreamInfTagAttribute::kVideo: |
| return "VIDEO"; |
| case XStreamInfTagAttribute::kAverageBandwidth: |
| return "AVERAGE-BANDWIDTH"; |
| case XStreamInfTagAttribute::kBandwidth: |
| return "BANDWIDTH"; |
| case XStreamInfTagAttribute::kCodecs: |
| return "CODECS"; |
| case XStreamInfTagAttribute::kFrameRate: |
| return "FRAME-RATE"; |
| case XStreamInfTagAttribute::kProgramId: |
| return "PROGRAM-ID"; |
| case XStreamInfTagAttribute::kResolution: |
| return "RESOLUTION"; |
| case XStreamInfTagAttribute::kScore: |
| return "SCORE"; |
| } |
| |
| NOTREACHED_NORETURN(); |
| } |
| |
| // Attributes expected in `EXT-X-SKIP` tag contents. |
| // These must remain sorted alphabetically. |
| enum class XSkipTagAttribute { |
| kRecentlyRemovedDateranges, |
| kSkippedSegments, |
| kMaxValue = kSkippedSegments, |
| }; |
| |
| constexpr std::string_view GetAttributeName(XSkipTagAttribute attribute) { |
| switch (attribute) { |
| case XSkipTagAttribute::kRecentlyRemovedDateranges: |
| return "RECENTLY-REMOVED-DATERANGES"; |
| case XSkipTagAttribute::kSkippedSegments: |
| return "SKIPPED-SEGMENTS"; |
| } |
| NOTREACHED_NORETURN(); |
| } |
| |
| enum class XRenditionReportTagAttribute { |
| kLastMSN, |
| kLastPart, |
| kUri, |
| kMaxValue = kUri, |
| }; |
| |
| constexpr std::string_view GetAttributeName(XRenditionReportTagAttribute attr) { |
| switch (attr) { |
| case XRenditionReportTagAttribute::kUri: |
| return "URI"; |
| case XRenditionReportTagAttribute::kLastMSN: |
| return "LAST-MSN"; |
| case XRenditionReportTagAttribute::kLastPart: |
| return "LAST-PART"; |
| } |
| NOTREACHED_NORETURN(); |
| } |
| |
| // Attributes expected in `EXT-X-MAP` tag contents. |
| // These must remain sorted alphabetically. |
| enum class XMapTagAttribute { |
| kByteRange, |
| kUri, |
| kMaxValue = kUri, |
| }; |
| |
| constexpr std::string_view GetAttributeName(XMapTagAttribute attribute) { |
| switch (attribute) { |
| case XMapTagAttribute::kByteRange: |
| return "BYTERANGE"; |
| case XMapTagAttribute::kUri: |
| return "URI"; |
| } |
| |
| NOTREACHED_NORETURN(); |
| } |
| |
| // Attributes expected in `EXT-X-PART` tag contents. |
| // These must remain sorted alphabetically. |
| enum class XPartTagAttribute { |
| kByteRange, |
| kDuration, |
| kGap, |
| kIndependent, |
| kUri, |
| kMaxValue = kUri, |
| }; |
| |
| constexpr std::string_view GetAttributeName(XPartTagAttribute attribute) { |
| switch (attribute) { |
| case XPartTagAttribute::kByteRange: |
| return "BYTERANGE"; |
| case XPartTagAttribute::kDuration: |
| return "DURATION"; |
| case XPartTagAttribute::kGap: |
| return "GAP"; |
| case XPartTagAttribute::kIndependent: |
| return "INDEPENDENT"; |
| case XPartTagAttribute::kUri: |
| return "URI"; |
| } |
| } |
| |
| // Attributes expected in `EXT-X-PART-INF` tag contents. |
| // These must remain sorted alphabetically. |
| enum class XPartInfTagAttribute { |
| kPartTarget, |
| kMaxValue = kPartTarget, |
| }; |
| |
| constexpr std::string_view GetAttributeName(XPartInfTagAttribute attribute) { |
| switch (attribute) { |
| case XPartInfTagAttribute::kPartTarget: |
| return "PART-TARGET"; |
| } |
| |
| NOTREACHED_NORETURN(); |
| } |
| |
| // Attributes expected in `EXT-X-SERVER-CONTROL` tag contents. |
| // These must remain sorted alphabetically. |
| enum class XServerControlTagAttribute { |
| kCanBlockReload, |
| kCanSkipDateRanges, |
| kCanSkipUntil, |
| kHoldBack, |
| kPartHoldBack, |
| kMaxValue = kPartHoldBack, |
| }; |
| |
| constexpr std::string_view GetAttributeName( |
| XServerControlTagAttribute attribute) { |
| switch (attribute) { |
| case XServerControlTagAttribute::kCanBlockReload: |
| return "CAN-BLOCK-RELOAD"; |
| case XServerControlTagAttribute::kCanSkipDateRanges: |
| return "CAN-SKIP-DATERANGES"; |
| case XServerControlTagAttribute::kCanSkipUntil: |
| return "CAN-SKIP-UNTIL"; |
| case XServerControlTagAttribute::kHoldBack: |
| return "HOLD-BACK"; |
| case XServerControlTagAttribute::kPartHoldBack: |
| return "PART-HOLD-BACK"; |
| } |
| |
| NOTREACHED_NORETURN(); |
| } |
| |
| enum class XKeyTagAttribute { |
| kIv, |
| kKeyFormat, |
| kKeyFormatVersions, |
| kMethod, |
| kUri, |
| kMaxValue = kUri, |
| }; |
| |
| constexpr std::string_view GetAttributeName(XKeyTagAttribute attribute) { |
| switch (attribute) { |
| case XKeyTagAttribute::kIv: |
| return "IV"; |
| case XKeyTagAttribute::kKeyFormat: |
| return "KEYFORMAT"; |
| case XKeyTagAttribute::kKeyFormatVersions: |
| return "KEYFORMATVERSIONS"; |
| case XKeyTagAttribute::kMethod: |
| return "METHOD"; |
| case XKeyTagAttribute::kUri: |
| return "URI"; |
| } |
| NOTREACHED_NORETURN(); |
| } |
| |
| template <typename T, size_t kLast> |
| constexpr bool IsAttributeEnumSorted(std::index_sequence<kLast>) { |
| return true; |
| } |
| |
| template <typename T, size_t kLHS, size_t kRHS, size_t... kRest> |
| constexpr bool IsAttributeEnumSorted( |
| std::index_sequence<kLHS, kRHS, kRest...>) { |
| const auto lhs = GetAttributeName(static_cast<T>(kLHS)); |
| const auto rhs = GetAttributeName(static_cast<T>(kRHS)); |
| return lhs < rhs && |
| IsAttributeEnumSorted<T>(std::index_sequence<kRHS, kRest...>{}); |
| } |
| |
| // Wraps `AttributeMap::MakeStorage` by mapping the (compile-time) sequence |
| // of size_t's to a sequence of the corresponding attribute enum names. |
| template <typename T, std::size_t... Indices> |
| constexpr std::array<types::AttributeMap::Item, sizeof...(Indices)> |
| MakeTypedAttributeMapStorage(std::index_sequence<Indices...> seq) { |
| static_assert(IsAttributeEnumSorted<T>(seq), |
| "Enum keys must be sorted alphabetically"); |
| return types::AttributeMap::MakeStorage( |
| GetAttributeName(static_cast<T>(Indices))...); |
| } |
| |
| // Helper for using AttributeMap with an enum of keys. |
| // The result of running `GetAttributeName` across `0..T::kMaxValue` (inclusive) |
| // must produced an ordered set of strings. |
| template <typename T> |
| struct TypedAttributeMap { |
| static_assert(std::is_enum<T>::value, "T must be an enum"); |
| static_assert(std::is_same<decltype(GetAttributeName(std::declval<T>())), |
| std::string_view>::value, |
| "GetAttributeName must be overloaded for T to return a " |
| "std::string_view"); |
| static constexpr size_t kNumKeys = static_cast<size_t>(T::kMaxValue) + 1; |
| |
| TypedAttributeMap() |
| : attributes_(MakeTypedAttributeMapStorage<T>( |
| std::make_index_sequence<kNumKeys>())) {} |
| |
| // Wraps `AttributeMap::FillUntilError` using the built-in storage object. |
| ParseStatus FillUntilError(types::AttributeListIterator* iter) { |
| types::AttributeMap map(attributes_); |
| return map.FillUntilError(iter); |
| } |
| |
| // Returns whether the entry corresponding to the given key has a value. |
| bool HasValue(T key) const { |
| return attributes_[static_cast<size_t>(key)].second.has_value(); |
| } |
| |
| // Returns the value stored in the entry for the given key. |
| SourceString GetValue(T key) const { |
| return attributes_[static_cast<size_t>(key)].second.value(); |
| } |
| |
| size_t Size() const { |
| return std::count_if(attributes_.begin(), attributes_.end(), |
| [](const types::AttributeMap::Item& item) { |
| return item.second.has_value(); |
| }); |
| } |
| |
| private: |
| std::array<types::AttributeMap::Item, kNumKeys> attributes_; |
| }; |
| |
| #define RETURN_IF_ERROR(var_not_expr) \ |
| do { \ |
| if (!var_not_expr.has_value()) { \ |
| return std::move(var_not_expr).error().AddHere(); \ |
| } \ |
| } while (0) |
| |
| template <template <typename...> typename SpecifiedContainer, typename Type> |
| struct is_specialization_of : std::false_type {}; |
| |
| template <template <typename...> typename SpecifiedContainer, typename... Types> |
| struct is_specialization_of<SpecifiedContainer, SpecifiedContainer<Types...>> |
| : std::true_type {}; |
| |
| // ParseField exists to help with the pattern where we need to parse some field |
| // from a dictionary of tag attributes where some of those fields are optional. |
| // It is designed to provide support for either optional or required fields, |
| // depending on the return-type specialization: |
| // When Result=std::optional<X>, ParseField will return a nullopt rather than |
| // an error if the `field_name` field can't be found in `map`. |
| // When Result=T (where T is not std::optional<X>), ParseField will fail with |
| // a ParseStatus if `field_name` is not found in map. |
| // A function pointer must also be provided which does the parsing from |
| // SourceString => Result, provided that the `field_name` key is present. |
| template <typename Result, |
| typename AttrEnum, |
| typename ParseFn, |
| typename... ParseFnArgs> |
| ParseStatus::Or<Result> ParseField(AttrEnum field_name, |
| TypedAttributeMap<AttrEnum> map, |
| ParseFn parser, |
| ParseFnArgs&&... args) { |
| if (map.HasValue(field_name)) { |
| auto maybe = |
| parser(map.GetValue(field_name), std::forward<ParseFnArgs>(args)...); |
| if (!maybe.has_value()) { |
| ParseStatus result = ParseStatusCode::kMalformedTag; |
| return std::move(result).AddCause(std::move(maybe).error()); |
| } |
| return Result(std::move(maybe).value()); |
| } |
| if constexpr (is_specialization_of<std::optional, Result>::value) { |
| return Result(std::nullopt); |
| } |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| } // namespace |
| |
| // static |
| ParseStatus::Or<M3uTag> M3uTag::Parse(TagItem tag) { |
| return ParseEmptyTag<M3uTag>(tag); |
| } |
| |
| // static |
| XDefineTag XDefineTag::CreateDefinition(types::VariableName name, |
| std::string_view value) { |
| return XDefineTag{.name = name, .value = value}; |
| } |
| |
| // static |
| XDefineTag XDefineTag::CreateImport(types::VariableName name) { |
| return XDefineTag{.name = name, .value = std::nullopt}; |
| } |
| |
| // static |
| ParseStatus::Or<XDefineTag> XDefineTag::Parse(TagItem tag) { |
| DCHECK(tag.GetName() == ToTagName(XDefineTag::kName)); |
| |
| if (!tag.GetContent().has_value()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| // Parse the attribute-list. |
| // Quoted strings in EXT-X-DEFINE tags are unique in that they aren't subject |
| // to variable substitution. For that reason, we use the |
| // `ParseQuotedStringWithoutSubstitution` function here. |
| TypedAttributeMap<XDefineTagAttribute> map; |
| types::AttributeListIterator iter(*tag.GetContent()); |
| auto result = map.FillUntilError(&iter); |
| |
| if (result.code() != ParseStatusCode::kReachedEOF) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(result)); |
| } |
| |
| // "NAME" and "IMPORT" are mutually exclusive |
| if (map.HasValue(XDefineTagAttribute::kName) && |
| map.HasValue(XDefineTagAttribute::kImport)) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| if (map.HasValue(XDefineTagAttribute::kName)) { |
| auto var_name = types::ParseQuotedStringWithoutSubstitution( |
| map.GetValue(XDefineTagAttribute::kName)) |
| .MapValue(types::VariableName::Parse); |
| if (!var_name.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(var_name).error()); |
| } |
| |
| // If "NAME" is defined, "VALUE" must also be defined |
| if (!map.HasValue(XDefineTagAttribute::kValue)) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| auto value = types::ParseQuotedStringWithoutSubstitution( |
| map.GetValue(XDefineTagAttribute::kValue), /*allow_empty*/ true); |
| if (!value.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag); |
| } |
| |
| return XDefineTag::CreateDefinition(std::move(var_name).value(), |
| std::move(value).value().Str()); |
| } |
| |
| if (map.HasValue(XDefineTagAttribute::kImport)) { |
| auto var_name = types::ParseQuotedStringWithoutSubstitution( |
| map.GetValue(XDefineTagAttribute::kImport)) |
| .MapValue(types::VariableName::Parse); |
| if (!var_name.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(var_name).error()); |
| } |
| |
| // "VALUE" doesn't make any sense here, but the spec doesn't explicitly |
| // forbid it. It may be used in the future to provide a default value for |
| // undefined imported variables, so we won't error on it. |
| return XDefineTag::CreateImport(std::move(var_name).value()); |
| } |
| |
| // Without "NAME" or "IMPORT", the tag is malformed |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| // static |
| ParseStatus::Or<XIndependentSegmentsTag> XIndependentSegmentsTag::Parse( |
| TagItem tag) { |
| return ParseEmptyTag<XIndependentSegmentsTag>(tag); |
| } |
| |
| // static |
| ParseStatus::Or<XVersionTag> XVersionTag::Parse(TagItem tag) { |
| auto result = ParseDecimalIntegerTag(tag, &XVersionTag::version); |
| if (!result.has_value()) { |
| return std::move(result).error(); |
| } |
| |
| // Reject invalid version numbers. |
| // For valid version numbers, caller will decide if the version is supported. |
| auto out = std::move(result).value(); |
| if (out.version == 0) { |
| return ParseStatusCode::kInvalidPlaylistVersion; |
| } |
| |
| return out; |
| } |
| |
| struct XMediaTag::CtorArgs { |
| decltype(XMediaTag::type) type; |
| decltype(XMediaTag::uri) uri; |
| decltype(XMediaTag::instream_id) instream_id; |
| decltype(XMediaTag::group_id) group_id; |
| decltype(XMediaTag::language) language; |
| decltype(XMediaTag::associated_language) associated_language; |
| decltype(XMediaTag::name) name; |
| decltype(XMediaTag::stable_rendition_id) stable_rendition_id; |
| decltype(XMediaTag::is_default) is_default; |
| decltype(XMediaTag::autoselect) autoselect; |
| decltype(XMediaTag::forced) forced; |
| decltype(XMediaTag::characteristics) characteristics; |
| decltype(XMediaTag::channels) channels; |
| }; |
| |
| XMediaTag::XMediaTag(CtorArgs args) |
| : type(std::move(args.type)), |
| uri(std::move(args.uri)), |
| instream_id(std::move(args.instream_id)), |
| group_id(std::move(args.group_id)), |
| language(std::move(args.language)), |
| associated_language(std::move(args.associated_language)), |
| name(std::move(args.name)), |
| stable_rendition_id(std::move(args.stable_rendition_id)), |
| is_default(std::move(args.is_default)), |
| autoselect(std::move(args.autoselect)), |
| forced(std::move(args.forced)), |
| characteristics(std::move(args.characteristics)), |
| channels(std::move(args.channels)) {} |
| |
| XMediaTag::~XMediaTag() = default; |
| |
| XMediaTag::XMediaTag(const XMediaTag&) = default; |
| |
| XMediaTag::XMediaTag(XMediaTag&&) = default; |
| |
| XMediaTag& XMediaTag::operator=(const XMediaTag&) = default; |
| |
| XMediaTag& XMediaTag::operator=(XMediaTag&&) = default; |
| |
| // static |
| ParseStatus::Or<XMediaTag> XMediaTag::Parse( |
| TagItem tag, |
| const VariableDictionary& variable_dict, |
| VariableDictionary::SubstitutionBuffer& sub_buffer) { |
| DCHECK(tag.GetName() == ToTagName(XMediaTag::kName)); |
| if (!tag.GetContent().has_value()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| // Parse the attribute-list |
| TypedAttributeMap<XMediaTagAttribute> map; |
| types::AttributeListIterator iter(*tag.GetContent()); |
| auto map_result = map.FillUntilError(&iter); |
| |
| if (map_result.code() != ParseStatusCode::kReachedEOF) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(map_result)); |
| } |
| |
| // Parse the 'TYPE' attribute |
| MediaType type; |
| if (map.HasValue(XMediaTagAttribute::kType)) { |
| auto str = map.GetValue(XMediaTagAttribute::kType); |
| if (str.Str() == "AUDIO") { |
| type = MediaType::kAudio; |
| } else if (str.Str() == "VIDEO") { |
| type = MediaType::kVideo; |
| } else if (str.Str() == "SUBTITLES") { |
| type = MediaType::kSubtitles; |
| } else if (str.Str() == "CLOSED-CAPTIONS") { |
| type = MediaType::kClosedCaptions; |
| } else { |
| return ParseStatusCode::kMalformedTag; |
| } |
| } else { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| // Parse the 'URI' attribute |
| std::optional<ResolvedSourceString> uri; |
| if (map.HasValue(XMediaTagAttribute::kUri)) { |
| // This attribute MUST NOT be defined for closed-captions renditions |
| if (type == MediaType::kClosedCaptions) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| auto result = types::ParseQuotedString( |
| map.GetValue(XMediaTagAttribute::kUri), variable_dict, sub_buffer); |
| if (!result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(result).error()); |
| } |
| |
| uri = std::move(result).value(); |
| } else { |
| // URI MUST be defined for subtitle renditions |
| if (type == MediaType::kSubtitles) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| } |
| |
| // Parse the 'GROUP-ID' attribute |
| std::optional<ResolvedSourceString> group_id; |
| if (map.HasValue(XMediaTagAttribute::kGroupId)) { |
| auto result = types::ParseQuotedString( |
| map.GetValue(XMediaTagAttribute::kGroupId), variable_dict, sub_buffer); |
| if (!result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(result).error()); |
| } |
| |
| group_id = std::move(result).value(); |
| } else { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| // Parse the 'LANGUAGE' attribute |
| std::optional<ResolvedSourceString> language; |
| if (map.HasValue(XMediaTagAttribute::kLanguage)) { |
| auto result = types::ParseQuotedString( |
| map.GetValue(XMediaTagAttribute::kLanguage), variable_dict, sub_buffer); |
| if (!result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(result).error()); |
| } |
| |
| language = std::move(result).value(); |
| } |
| |
| // Parse the 'ASSOC-LANGUAGE' attribute |
| std::optional<ResolvedSourceString> assoc_language; |
| if (map.HasValue(XMediaTagAttribute::kAssocLanguage)) { |
| auto result = types::ParseQuotedString( |
| map.GetValue(XMediaTagAttribute::kAssocLanguage), variable_dict, |
| sub_buffer); |
| if (!result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(result).error()); |
| } |
| |
| assoc_language = std::move(result).value(); |
| } |
| |
| // Parse the 'NAME' attribute |
| std::optional<ResolvedSourceString> name; |
| if (map.HasValue(XMediaTagAttribute::kName)) { |
| auto result = types::ParseQuotedString( |
| map.GetValue(XMediaTagAttribute::kName), variable_dict, sub_buffer); |
| if (!result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(result).error()); |
| } |
| |
| name = std::move(result).value(); |
| } else { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| // Parse the 'STABLE-RENDITION-ID' attribute |
| std::optional<types::StableId> stable_rendition_id; |
| if (map.HasValue(XMediaTagAttribute::kStableRenditionId)) { |
| auto result = types::ParseQuotedString( |
| map.GetValue(XMediaTagAttribute::kStableRenditionId), |
| variable_dict, sub_buffer) |
| .MapValue(types::StableId::Parse); |
| if (!result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(result).error()); |
| } |
| stable_rendition_id = std::move(result).value(); |
| } |
| |
| // Parse the 'DEFAULT' attribute |
| bool is_default = false; |
| if (map.HasValue(XMediaTagAttribute::kDefault)) { |
| if (map.GetValue(XMediaTagAttribute::kDefault).Str() == "YES") { |
| is_default = true; |
| } |
| } |
| |
| // Parse the 'AUTOSELECT' attribute |
| bool autoselect = is_default; |
| if (map.HasValue(XMediaTagAttribute::kAutoselect)) { |
| if (map.GetValue(XMediaTagAttribute::kAutoselect).Str() == "YES") { |
| autoselect = true; |
| } else if (is_default) { |
| // If the 'DEFAULT' attribute is 'YES', then the value of this attribute |
| // must also be 'YES', if present. |
| return ParseStatusCode::kMalformedTag; |
| } |
| } |
| |
| // Parse the 'FORCED' attribute |
| bool forced = false; |
| if (map.HasValue(XMediaTagAttribute::kForced)) { |
| // The FORCED attribute MUST NOT be present unless TYPE=SUBTITLES |
| if (type != MediaType::kSubtitles) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| if (map.GetValue(XMediaTagAttribute::kForced).Str() == "YES") { |
| forced = true; |
| } |
| } |
| |
| // Parse the 'INSTREAM-ID' attribute |
| std::optional<types::InstreamId> instream_id; |
| if (map.HasValue(XMediaTagAttribute::kInstreamId)) { |
| // The INSTREAM-ID attribute MUST NOT be present unless TYPE=CLOSED-CAPTIONS |
| if (type != MediaType::kClosedCaptions) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| auto result = |
| types::ParseQuotedString(map.GetValue(XMediaTagAttribute::kInstreamId), |
| variable_dict, sub_buffer) |
| .MapValue(types::InstreamId::Parse); |
| if (!result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(result).error()); |
| } |
| |
| instream_id = std::move(result).value(); |
| } |
| // This attribute is REQUIRED if TYPE=CLOSED-CAPTIONS |
| else if (type == MediaType::kClosedCaptions) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| // Parse the 'CHARACTERISTICS' attribute |
| std::vector<std::string> characteristics; |
| if (map.HasValue(XMediaTagAttribute::kCharacteristics)) { |
| auto result = types::ParseQuotedString( |
| map.GetValue(XMediaTagAttribute::kCharacteristics), variable_dict, |
| sub_buffer); |
| if (!result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(result).error()); |
| } |
| auto value = std::move(result).value(); |
| |
| while (!value.Empty()) { |
| const auto mct = value.ConsumeDelimiter(','); |
| if (mct.Empty()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| characteristics.emplace_back(mct.Str()); |
| } |
| } |
| |
| // Parse the 'CHANNELS' attribute |
| std::optional<types::AudioChannels> channels; |
| if (map.HasValue(XMediaTagAttribute::kChannels)) { |
| // Currently only supported type with channel information is `kAudio`. |
| if (type == MediaType::kAudio) { |
| auto result = |
| types::ParseQuotedString(map.GetValue(XMediaTagAttribute::kChannels), |
| variable_dict, sub_buffer) |
| .MapValue(types::AudioChannels::Parse); |
| if (!result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(result).error()); |
| } |
| |
| channels = std::move(result).value(); |
| } |
| } |
| |
| return XMediaTag(XMediaTag::CtorArgs{ |
| .type = type, |
| .uri = uri, |
| .instream_id = instream_id, |
| .group_id = group_id.value(), |
| .language = language, |
| .associated_language = assoc_language, |
| .name = name.value(), |
| .stable_rendition_id = std::move(stable_rendition_id), |
| .is_default = is_default, |
| .autoselect = autoselect, |
| .forced = forced, |
| .characteristics = std::move(characteristics), |
| .channels = std::move(channels), |
| }); |
| } |
| |
| XStreamInfTag::XStreamInfTag() = default; |
| |
| XStreamInfTag::~XStreamInfTag() = default; |
| |
| XStreamInfTag::XStreamInfTag(const XStreamInfTag&) = default; |
| |
| XStreamInfTag::XStreamInfTag(XStreamInfTag&&) = default; |
| |
| XStreamInfTag& XStreamInfTag::operator=(const XStreamInfTag&) = default; |
| |
| XStreamInfTag& XStreamInfTag::operator=(XStreamInfTag&&) = default; |
| |
| // static |
| ParseStatus::Or<XStreamInfTag> XStreamInfTag::Parse( |
| TagItem tag, |
| const VariableDictionary& variable_dict, |
| VariableDictionary::SubstitutionBuffer& sub_buffer) { |
| DCHECK(tag.GetName() == ToTagName(XStreamInfTag::kName)); |
| XStreamInfTag out; |
| |
| if (!tag.GetContent().has_value()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| // Parse the attribute-list |
| TypedAttributeMap<XStreamInfTagAttribute> map; |
| types::AttributeListIterator iter(*tag.GetContent()); |
| auto map_result = map.FillUntilError(&iter); |
| |
| if (map_result.code() != ParseStatusCode::kReachedEOF) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(map_result)); |
| } |
| |
| // Extract the 'BANDWIDTH' attribute |
| if (map.HasValue(XStreamInfTagAttribute::kBandwidth)) { |
| auto bandwidth = types::ParseDecimalInteger( |
| map.GetValue(XStreamInfTagAttribute::kBandwidth) |
| .SkipVariableSubstitution()); |
| if (!bandwidth.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(bandwidth).error()); |
| } |
| |
| out.bandwidth = std::move(bandwidth).value(); |
| } else { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| // Extract the 'AVERAGE-BANDWIDTH' attribute |
| if (map.HasValue(XStreamInfTagAttribute::kAverageBandwidth)) { |
| auto average_bandwidth = types::ParseDecimalInteger( |
| map.GetValue(XStreamInfTagAttribute::kAverageBandwidth) |
| .SkipVariableSubstitution()); |
| if (!average_bandwidth.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(average_bandwidth).error()); |
| } |
| |
| out.average_bandwidth = std::move(average_bandwidth).value(); |
| } |
| |
| // Extract the 'SCORE' attribute |
| if (map.HasValue(XStreamInfTagAttribute::kScore)) { |
| auto score = types::ParseDecimalFloatingPoint( |
| map.GetValue(XStreamInfTagAttribute::kScore) |
| .SkipVariableSubstitution()); |
| if (!score.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(score).error()); |
| } |
| |
| out.score = std::move(score).value(); |
| } |
| |
| // Extract the 'CODECS' attribute |
| if (map.HasValue(XStreamInfTagAttribute::kCodecs)) { |
| auto codecs_string = |
| types::ParseQuotedString(map.GetValue(XStreamInfTagAttribute::kCodecs), |
| variable_dict, sub_buffer); |
| if (!codecs_string.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(codecs_string).error()); |
| } |
| |
| // Split the list of codecs |
| std::vector<std::string> codecs; |
| SplitCodecs(std::move(codecs_string).value().Str(), &codecs); |
| out.codecs = std::move(codecs); |
| } |
| |
| // Extract the 'RESOLUTION' attribute |
| if (map.HasValue(XStreamInfTagAttribute::kResolution)) { |
| auto resolution = types::DecimalResolution::Parse( |
| map.GetValue(XStreamInfTagAttribute::kResolution) |
| .SkipVariableSubstitution()); |
| if (!resolution.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(resolution).error()); |
| } |
| out.resolution = std::move(resolution).value(); |
| } |
| |
| // Extract the 'FRAME-RATE' attribute |
| if (map.HasValue(XStreamInfTagAttribute::kFrameRate)) { |
| auto frame_rate = types::ParseDecimalFloatingPoint( |
| map.GetValue(XStreamInfTagAttribute::kFrameRate) |
| .SkipVariableSubstitution()); |
| if (!frame_rate.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(frame_rate).error()); |
| } |
| out.frame_rate = std::move(frame_rate).value(); |
| } |
| |
| // Extract the 'AUDIO' attribute |
| if (map.HasValue(XStreamInfTagAttribute::kAudio)) { |
| auto audio = |
| types::ParseQuotedString(map.GetValue(XStreamInfTagAttribute::kAudio), |
| variable_dict, sub_buffer); |
| if (!audio.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(audio).error()); |
| } |
| out.audio = std::move(audio).value(); |
| } |
| |
| // Extract the 'VIDEO' attribute |
| if (map.HasValue(XStreamInfTagAttribute::kVideo)) { |
| auto video = |
| types::ParseQuotedString(map.GetValue(XStreamInfTagAttribute::kVideo), |
| variable_dict, sub_buffer); |
| if (!video.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(video).error()); |
| } |
| out.video = std::move(video).value(); |
| } |
| |
| return out; |
| } |
| |
| // static |
| ParseStatus::Or<InfTag> InfTag::Parse(TagItem tag) { |
| DCHECK(tag.GetName() == ToTagName(InfTag::kName)); |
| |
| if (!tag.GetContent()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| auto content = *tag.GetContent(); |
| |
| // Inf tags have the form #EXTINF:<duration>,[<title>] |
| // Find the comma. |
| auto comma = content.Str().find_first_of(','); |
| SourceString duration_str = content; |
| SourceString title_str = content; |
| if (comma == std::string_view::npos) { |
| // While the HLS spec does require commas at the end of inf tags, it's |
| // incredibly common for sites to elide the comma if there is no title |
| // attribute present. In this case, we should assert that there is at least |
| // a trailing newline, and then strip it to generate a nameless tag. |
| title_str = content.Substr(0, 0); |
| if (*content.Str().end() != '\n') { |
| duration_str = content; |
| } else { |
| duration_str = content.Substr(0, content.Str().length() - 1); |
| } |
| } else { |
| duration_str = content.Substr(0, comma); |
| title_str = content.Substr(comma + 1); |
| } |
| |
| // Extract duration |
| // TODO(crbug.com/40210233): Below version 3 this should be rounded to an |
| // integer |
| auto duration_result = |
| types::ParseDecimalFloatingPoint(duration_str.SkipVariableSubstitution()); |
| if (!duration_result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(duration_result).error()); |
| } |
| const auto duration = base::Seconds(std::move(duration_result).value()); |
| |
| if (duration.is_max()) { |
| return ParseStatusCode::kValueOverflowsTimeDelta; |
| } |
| |
| return InfTag{.duration = duration, .title = title_str}; |
| } |
| |
| // static |
| ParseStatus::Or<XBitrateTag> XBitrateTag::Parse(TagItem tag) { |
| return ParseDecimalIntegerTag(tag, &XBitrateTag::bitrate); |
| } |
| |
| // static |
| ParseStatus::Or<XByteRangeTag> XByteRangeTag::Parse(TagItem tag) { |
| DCHECK(tag.GetName() == ToTagName(XByteRangeTag::kName)); |
| if (!tag.GetContent().has_value()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| auto range = types::parsing::ByteRangeExpression::Parse( |
| tag.GetContent()->SkipVariableSubstitution()); |
| if (!range.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(range).error()); |
| } |
| |
| return XByteRangeTag{.range = std::move(range).value()}; |
| } |
| |
| // static |
| ParseStatus::Or<XDiscontinuityTag> XDiscontinuityTag::Parse(TagItem tag) { |
| return ParseEmptyTag<XDiscontinuityTag>(tag); |
| } |
| |
| // static |
| ParseStatus::Or<XDiscontinuitySequenceTag> XDiscontinuitySequenceTag::Parse( |
| TagItem tag) { |
| return ParseDecimalIntegerTag(tag, &XDiscontinuitySequenceTag::number); |
| } |
| |
| // static |
| ParseStatus::Or<XEndListTag> XEndListTag::Parse(TagItem tag) { |
| return ParseEmptyTag<XEndListTag>(tag); |
| } |
| |
| // static |
| ParseStatus::Or<XGapTag> XGapTag::Parse(TagItem tag) { |
| return ParseEmptyTag<XGapTag>(tag); |
| } |
| |
| // static |
| ParseStatus::Or<XIFramesOnlyTag> XIFramesOnlyTag::Parse(TagItem tag) { |
| return ParseEmptyTag<XIFramesOnlyTag>(tag); |
| } |
| |
| // static |
| ParseStatus::Or<XMapTag> XMapTag::Parse( |
| TagItem tag, |
| const VariableDictionary& variable_dict, |
| VariableDictionary::SubstitutionBuffer& sub_buffer) { |
| DCHECK(tag.GetName() == ToTagName(XMapTag::kName)); |
| if (!tag.GetContent().has_value()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| // Parse the attribute-list |
| TypedAttributeMap<XMapTagAttribute> map; |
| types::AttributeListIterator iter(*tag.GetContent()); |
| auto map_result = map.FillUntilError(&iter); |
| |
| if (map_result.code() != ParseStatusCode::kReachedEOF) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(map_result)); |
| } |
| |
| std::optional<ResolvedSourceString> uri; |
| if (map.HasValue(XMapTagAttribute::kUri)) { |
| auto result = types::ParseQuotedString(map.GetValue(XMapTagAttribute::kUri), |
| variable_dict, sub_buffer); |
| if (!result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(result).error()); |
| } |
| |
| uri = std::move(result).value(); |
| } else { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| std::optional<types::parsing::ByteRangeExpression> byte_range; |
| if (map.HasValue(XMapTagAttribute::kByteRange)) { |
| auto result = |
| types::ParseQuotedString(map.GetValue(XMapTagAttribute::kByteRange), |
| variable_dict, sub_buffer) |
| .MapValue(types::parsing::ByteRangeExpression::Parse); |
| if (!result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(result).error()); |
| } |
| |
| byte_range = std::move(result).value(); |
| } |
| |
| return XMapTag{.uri = uri.value(), .byte_range = byte_range}; |
| } |
| |
| // static |
| ParseStatus::Or<XMediaSequenceTag> XMediaSequenceTag::Parse(TagItem tag) { |
| return ParseDecimalIntegerTag(tag, &XMediaSequenceTag::number); |
| } |
| |
| // static |
| ParseStatus::Or<XPartTag> XPartTag::Parse( |
| TagItem tag, |
| const VariableDictionary& vars, |
| VariableDictionary::SubstitutionBuffer& subs) { |
| DCHECK(tag.GetName() == ToTagName(XPartTag::kName)); |
| if (!tag.GetContent().has_value()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| TypedAttributeMap<XPartTagAttribute> map; |
| types::AttributeListIterator iter(*tag.GetContent()); |
| auto map_result = map.FillUntilError(&iter); |
| |
| if (map_result.code() != ParseStatusCode::kReachedEOF) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(map_result)); |
| } |
| |
| auto uri = ParseField<ResolvedSourceString>( |
| XPartTagAttribute::kUri, map, |
| &types::parsing::Quoted<types::parsing::RawStr>::ParseWithSubstitution, |
| vars, subs); |
| RETURN_IF_ERROR(uri); |
| |
| auto duration = ParseField<base::TimeDelta>( |
| XPartTagAttribute::kDuration, map, |
| &types::parsing::TimeDelta::ParseWithoutSubstitution); |
| RETURN_IF_ERROR(duration); |
| |
| auto byte_range = |
| ParseField<std::optional<types::parsing::ByteRangeExpression>>( |
| XPartTagAttribute::kByteRange, map, |
| &types::parsing::Quoted< |
| types::parsing::ByteRangeExpression>::ParseWithSubstitution, |
| vars, subs); |
| RETURN_IF_ERROR(byte_range); |
| |
| auto independent = ParseField<std::optional<bool>>( |
| XPartTagAttribute::kIndependent, map, |
| &types::parsing::YesOrNo::ParseWithoutSubstitution); |
| RETURN_IF_ERROR(independent); |
| |
| auto gap = ParseField<std::optional<bool>>( |
| XPartTagAttribute::kGap, map, |
| &types::parsing::YesOrNo::ParseWithoutSubstitution); |
| RETURN_IF_ERROR(gap); |
| |
| return XPartTag{.uri = std::move(uri).value(), |
| .duration = std::move(duration).value(), |
| .byte_range = std::move(byte_range).value(), |
| .independent = (*independent).value_or(false), |
| .gap = (*gap).value_or(false)}; |
| } |
| |
| // static |
| ParseStatus::Or<XPartInfTag> XPartInfTag::Parse(TagItem tag) { |
| DCHECK(tag.GetName() == ToTagName(XPartInfTag::kName)); |
| if (!tag.GetContent().has_value()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| // Parse the attribute-list |
| TypedAttributeMap<XPartInfTagAttribute> map; |
| types::AttributeListIterator iter(*tag.GetContent()); |
| auto map_result = map.FillUntilError(&iter); |
| |
| if (map_result.code() != ParseStatusCode::kReachedEOF) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(map_result)); |
| } |
| |
| // Extract the 'PART-TARGET' attribute |
| base::TimeDelta part_target; |
| if (map.HasValue(XPartInfTagAttribute::kPartTarget)) { |
| auto result = types::ParseDecimalFloatingPoint( |
| map.GetValue(XPartInfTagAttribute::kPartTarget) |
| .SkipVariableSubstitution()); |
| |
| if (!result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(result).error()); |
| } |
| |
| part_target = base::Seconds(std::move(result).value()); |
| |
| if (part_target.is_max()) { |
| return ParseStatusCode::kValueOverflowsTimeDelta; |
| } |
| } else { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| return XPartInfTag{.target_duration = part_target}; |
| } |
| |
| // static |
| ParseStatus::Or<XPlaylistTypeTag> XPlaylistTypeTag::Parse(TagItem tag) { |
| DCHECK(tag.GetName() == ToTagName(XPlaylistTypeTag::kName)); |
| |
| // This tag requires content |
| if (!tag.GetContent().has_value() || tag.GetContent()->Empty()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| if (tag.GetContent()->Str() == "EVENT") { |
| return XPlaylistTypeTag{.type = PlaylistType::kEvent}; |
| } |
| if (tag.GetContent()->Str() == "VOD") { |
| return XPlaylistTypeTag{.type = PlaylistType::kVOD}; |
| } |
| |
| return ParseStatusCode::kUnknownPlaylistType; |
| } |
| |
| // static |
| ParseStatus::Or<XServerControlTag> XServerControlTag::Parse(TagItem tag) { |
| DCHECK(tag.GetName() == ToTagName(XServerControlTag::kName)); |
| if (!tag.GetContent().has_value()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| // Parse the attribute-list |
| TypedAttributeMap<XServerControlTagAttribute> map; |
| types::AttributeListIterator iter(*tag.GetContent()); |
| auto map_result = map.FillUntilError(&iter); |
| |
| if (map_result.code() != ParseStatusCode::kReachedEOF) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(map_result)); |
| } |
| |
| // Extract the 'CAN-SKIP-UNTIL' attribute |
| std::optional<base::TimeDelta> can_skip_until; |
| if (map.HasValue(XServerControlTagAttribute::kCanSkipUntil)) { |
| auto result = types::ParseDecimalFloatingPoint( |
| map.GetValue(XServerControlTagAttribute::kCanSkipUntil) |
| .SkipVariableSubstitution()); |
| |
| if (!result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(result).error()); |
| } |
| |
| can_skip_until = base::Seconds(std::move(result).value()); |
| |
| if (can_skip_until->is_max()) { |
| return ParseStatusCode::kValueOverflowsTimeDelta; |
| } |
| } |
| |
| // Extract the 'CAN-SKIP-DATERANGES' attribute |
| bool can_skip_dateranges = false; |
| if (map.HasValue(XServerControlTagAttribute::kCanSkipDateRanges)) { |
| if (map.GetValue(XServerControlTagAttribute::kCanSkipDateRanges).Str() == |
| "YES") { |
| // The existence of this attribute requires the 'CAN-SKIP-UNTIL' |
| // attribute. |
| if (!can_skip_until.has_value()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| can_skip_dateranges = true; |
| } |
| } |
| |
| // Extract the 'HOLD-BACK' attribute |
| std::optional<base::TimeDelta> hold_back; |
| if (map.HasValue(XServerControlTagAttribute::kHoldBack)) { |
| auto result = types::ParseDecimalFloatingPoint( |
| map.GetValue(XServerControlTagAttribute::kHoldBack) |
| .SkipVariableSubstitution()); |
| |
| if (!result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(result).error()); |
| } |
| |
| hold_back = base::Seconds(std::move(result).value()); |
| |
| if (hold_back->is_max()) { |
| return ParseStatusCode::kValueOverflowsTimeDelta; |
| } |
| } |
| |
| // Extract the 'PART-HOLD-BACK' attribute |
| std::optional<base::TimeDelta> part_hold_back; |
| if (map.HasValue(XServerControlTagAttribute::kPartHoldBack)) { |
| auto result = types::ParseDecimalFloatingPoint( |
| map.GetValue(XServerControlTagAttribute::kPartHoldBack) |
| .SkipVariableSubstitution()); |
| |
| if (!result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(result).error()); |
| } |
| |
| part_hold_back = base::Seconds(std::move(result).value()); |
| |
| if (part_hold_back->is_max()) { |
| return ParseStatusCode::kValueOverflowsTimeDelta; |
| } |
| } |
| |
| // Extract the 'CAN-BLOCK-RELOAD' attribute |
| bool can_block_reload = false; |
| if (map.HasValue(XServerControlTagAttribute::kCanBlockReload)) { |
| if (map.GetValue(XServerControlTagAttribute::kCanBlockReload).Str() == |
| "YES") { |
| can_block_reload = true; |
| } |
| } |
| |
| return XServerControlTag{ |
| .skip_boundary = can_skip_until, |
| .can_skip_dateranges = can_skip_dateranges, |
| .hold_back = hold_back, |
| .part_hold_back = part_hold_back, |
| .can_block_reload = can_block_reload, |
| }; |
| } |
| |
| // static |
| ParseStatus::Or<XTargetDurationTag> XTargetDurationTag::Parse(TagItem tag) { |
| DCHECK(tag.GetName() == ToTagName(XTargetDurationTag::kName)); |
| if (!tag.GetContent().has_value()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| auto duration_result = types::ParseDecimalInteger( |
| tag.GetContent().value().SkipVariableSubstitution()); |
| if (!duration_result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(duration_result).error()); |
| } |
| |
| auto duration = base::Seconds(std::move(duration_result).value()); |
| if (duration.is_max()) { |
| return ParseStatusCode::kValueOverflowsTimeDelta; |
| } |
| |
| return XTargetDurationTag{.duration = duration}; |
| } |
| |
| XSkipTag::XSkipTag() = default; |
| XSkipTag::~XSkipTag() = default; |
| XSkipTag::XSkipTag(const XSkipTag&) = default; |
| XSkipTag::XSkipTag(XSkipTag&&) = default; |
| |
| ParseStatus::Or<XSkipTag> XSkipTag::Parse( |
| TagItem tag, |
| const VariableDictionary& variable_dict, |
| VariableDictionary::SubstitutionBuffer& sub_buffer) { |
| DCHECK(tag.GetName() == ToTagName(XSkipTag::kName)); |
| if (!tag.GetContent().has_value()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| XSkipTag out; |
| TypedAttributeMap<XSkipTagAttribute> map; |
| types::AttributeListIterator iter(*tag.GetContent()); |
| auto map_result = map.FillUntilError(&iter); |
| |
| if (map_result.code() != ParseStatusCode::kReachedEOF) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(map_result)); |
| } |
| |
| if (!map.HasValue(XSkipTagAttribute::kSkippedSegments)) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| auto skip_result = types::ParseDecimalInteger( |
| map.GetValue(XSkipTagAttribute::kSkippedSegments) |
| .SkipVariableSubstitution()); |
| |
| if (!skip_result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(skip_result).error()); |
| } |
| |
| out.skipped_segments = std::move(skip_result).value(); |
| |
| if (map.HasValue(XSkipTagAttribute::kRecentlyRemovedDateranges)) { |
| // TODO(bug/314833987): Should this list have substitution? |
| auto removed_result = types::ParseQuotedString( |
| map.GetValue(XSkipTagAttribute::kRecentlyRemovedDateranges), |
| variable_dict, sub_buffer, /*allow_empty=*/true); |
| if (!removed_result.has_value()) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(removed_result).error()); |
| } |
| |
| auto tab_joined_daterange_ids = std::move(removed_result).value(); |
| std::vector<std::string> removed_dateranges = {}; |
| |
| while (!tab_joined_daterange_ids.Empty()) { |
| const auto daterange_id = tab_joined_daterange_ids.ConsumeDelimiter('\t'); |
| if (daterange_id.Empty()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| // TODO(bug/314833987): What type should this be parsed into? |
| removed_dateranges.emplace_back(daterange_id.Str()); |
| } |
| out.recently_removed_dateranges = std::move(removed_dateranges); |
| } |
| |
| return out; |
| } |
| |
| ParseStatus::Or<XRenditionReportTag> XRenditionReportTag::Parse( |
| TagItem tag, |
| const VariableDictionary& variable_dict, |
| VariableDictionary::SubstitutionBuffer& sub_buffer) { |
| CHECK(tag.GetName() == ToTagName(XRenditionReportTag::kName)); |
| if (!tag.GetContent().has_value()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| XRenditionReportTag out; |
| TypedAttributeMap<XRenditionReportTagAttribute> map; |
| types::AttributeListIterator iter(*tag.GetContent()); |
| auto map_result = map.FillUntilError(&iter); |
| |
| if (map_result.code() != ParseStatusCode::kReachedEOF) { |
| return std::move(map_result).AddHere(); |
| } |
| |
| if (map.HasValue(XRenditionReportTagAttribute::kUri)) { |
| auto uri_result = types::ParseQuotedString( |
| map.GetValue(XRenditionReportTagAttribute::kUri), variable_dict, |
| sub_buffer); |
| if (!uri_result.has_value()) { |
| return std::move(uri_result).error().AddHere(); |
| } |
| out.uri = std::move(uri_result).value(); |
| } |
| |
| if (map.HasValue(XRenditionReportTagAttribute::kLastMSN)) { |
| auto msn_result = types::ParseDecimalInteger( |
| map.GetValue(XRenditionReportTagAttribute::kLastMSN) |
| .SkipVariableSubstitution()); |
| if (!msn_result.has_value()) { |
| return std::move(msn_result).error().AddHere(); |
| } |
| out.last_msn = std::move(msn_result).value(); |
| } |
| |
| if (map.HasValue(XRenditionReportTagAttribute::kLastPart)) { |
| auto part_result = types::ParseDecimalInteger( |
| map.GetValue(XRenditionReportTagAttribute::kLastPart) |
| .SkipVariableSubstitution()); |
| if (!part_result.has_value()) { |
| return std::move(part_result).error().AddHere(); |
| } |
| out.last_part = std::move(part_result).value(); |
| } |
| |
| return out; |
| } |
| |
| ParseStatus::Or<XProgramDateTimeTag> XProgramDateTimeTag::Parse(TagItem tag) { |
| return ParseISO8601DateTimeTag(tag, &XProgramDateTimeTag::time); |
| } |
| |
| template <typename Attrs> |
| ParseStatus::Or<TypedAttributeMap<Attrs>> RequireNonEmptyMap( |
| std::optional<SourceString> content) { |
| if (!content.has_value()) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| |
| TypedAttributeMap<Attrs> map; |
| types::AttributeListIterator iter(*content); |
| auto map_result = map.FillUntilError(&iter); |
| |
| if (map_result.code() != ParseStatusCode::kReachedEOF) { |
| return ParseStatus(ParseStatusCode::kMalformedTag) |
| .AddCause(std::move(map_result)); |
| } |
| |
| return map; |
| } |
| |
| namespace { |
| |
| constexpr char const* kMethodNone = "NONE"; |
| constexpr char const* kMethodAES128 = "AES-128"; |
| constexpr char const* kMethodAES256 = "AES-256"; |
| constexpr char const* kMethodSampleAES = "SAMPLE-AES"; |
| constexpr char const* kMethodSampleAESCTR = "SAMPLE-AES-CTR"; |
| constexpr char const* kMethodSampleAESCENC = "SAMPLE-AES-CENC"; |
| constexpr char const* kMethodISO230017 = "ISO-23001-7"; |
| |
| constexpr char const* kFmtClearkey = "org.w3.clearkey"; |
| constexpr char const* kFmtWidevine = |
| "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"; |
| constexpr char const* kFmtIdentity = "identity"; |
| |
| ParseStatus::Or<XKeyTagMethod> RecognizeMethod(SourceString content) { |
| if (content.Str() == kMethodNone) { |
| return XKeyTagMethod::kNone; |
| } |
| if (content.Str() == kMethodAES128) { |
| return XKeyTagMethod::kAES128; |
| } else if (content.Str() == kMethodAES256) { |
| return XKeyTagMethod::kAES256; |
| } else if (content.Str() == kMethodSampleAES) { |
| return XKeyTagMethod::kSampleAES; |
| } else if (content.Str() == kMethodSampleAESCTR) { |
| return XKeyTagMethod::kSampleAESCTR; |
| } else if (content.Str() == kMethodSampleAESCENC) { |
| return XKeyTagMethod::kSampleAESCENC; |
| } else if (content.Str() == kMethodISO230017) { |
| return XKeyTagMethod::kISO230017; |
| } else { |
| return ParseStatusCode::kMalformedTag; |
| } |
| } |
| |
| ParseStatus::Or<XKeyTagKeyFormat> RecognizeFormat( |
| std::optional<ResolvedSourceString> content) { |
| if (!content.has_value()) { |
| return XKeyTagKeyFormat::kIdentity; |
| } else if (content->Str() == kFmtClearkey) { |
| return XKeyTagKeyFormat::kClearKey; |
| } else if (content->Str() == kFmtWidevine) { |
| return XKeyTagKeyFormat::kWidevine; |
| } else if (content->Str() == kFmtIdentity) { |
| return XKeyTagKeyFormat::kIdentity; |
| } else { |
| return XKeyTagKeyFormat::kUnsupported; |
| } |
| } |
| |
| template <typename T> |
| ParseStatus::Or<T> ValidateKeyTag( |
| XKeyTagMethod method, |
| ResolvedSourceString uri, |
| std::optional<XKeyTag::IVHex::Container> iv, |
| XKeyTagKeyFormat keyformat, |
| std::optional<ResolvedSourceString> keyformat_versions) { |
| // Check for incompatible fields. Some methods _must not_ have an IV |
| // present, while some formats only have a specific set of allowable |
| // methods. |
| if (iv.has_value()) { |
| switch (method) { |
| case XKeyTagMethod::kSampleAESCTR: |
| case XKeyTagMethod::kSampleAESCENC: |
| case XKeyTagMethod::kISO230017: |
| return ParseStatusCode::kMalformedTag; |
| default: |
| break; |
| } |
| } |
| switch (keyformat) { |
| case XKeyTagKeyFormat::kIdentity: |
| case XKeyTagKeyFormat::kUnsupported: |
| break; // Identity and others always are ok. |
| case XKeyTagKeyFormat::kClearKey: |
| case XKeyTagKeyFormat::kWidevine: |
| switch (method) { |
| case XKeyTagMethod::kSampleAES: |
| case XKeyTagMethod::kSampleAESCTR: |
| case XKeyTagMethod::kSampleAESCENC: |
| case XKeyTagMethod::kISO230017: |
| break; // Acceptable methods for widevine and clearkey |
| case XKeyTagMethod::kNone: |
| case XKeyTagMethod::kAES128: |
| case XKeyTagMethod::kAES256: |
| return ParseStatusCode::kMalformedTag; |
| } |
| break; |
| } |
| |
| return T{.method = method, |
| .uri = std::move(uri), |
| .iv = std::move(iv), |
| .keyformat = keyformat, |
| .keyformat_versions = std::move(keyformat_versions)}; |
| } |
| |
| template <typename T> |
| ParseStatus::Or<T> ParseKeyTag(TagItem tag, |
| const VariableDictionary& vars, |
| VariableDictionary::SubstitutionBuffer& subs) { |
| return RequireNonEmptyMap<XKeyTagAttribute>(tag.GetContent()) |
| .MapValue([&](auto map) -> ParseStatus::Or<T> { |
| auto enc_method = ParseField<XKeyTagMethod>(XKeyTagAttribute::kMethod, |
| map, &RecognizeMethod); |
| RETURN_IF_ERROR(enc_method); |
| |
| if constexpr (T::kAllowEmptyMethod) { |
| if (*enc_method == XKeyTagMethod::kNone) { |
| if (map.Size() != 1) { |
| return ParseStatusCode::kMalformedTag; |
| } |
| return T{.method = *enc_method}; |
| } |
| } |
| |
| auto enc_uri = ParseField<ResolvedSourceString>( |
| XKeyTagAttribute::kUri, map, |
| &types::parsing::Quoted< |
| types::parsing::RawStr>::ParseWithSubstitution, |
| vars, subs); |
| RETURN_IF_ERROR(enc_uri); |
| |
| auto enc_iv = ParseField<std::optional<XKeyTag::IVHex::Container>>( |
| XKeyTagAttribute::kIv, map, |
| &XKeyTag::IVHex::ParseWithoutSubstitution); |
| RETURN_IF_ERROR(enc_iv); |
| |
| auto enc_keyformat = |
| ParseField<std::optional<ResolvedSourceString>>( |
| XKeyTagAttribute::kKeyFormat, map, |
| &types::parsing::Quoted< |
| types::parsing::RawStr>::ParseWithoutSubstitution) |
| .MapValue(&RecognizeFormat); |
| RETURN_IF_ERROR(enc_keyformat); |
| |
| auto enc_keyformat_versions = |
| ParseField<std::optional<ResolvedSourceString>>( |
| XKeyTagAttribute::kKeyFormatVersions, map, |
| &types::parsing::RawStr::ParseWithoutSubstitution); |
| RETURN_IF_ERROR(enc_keyformat_versions); |
| |
| return ValidateKeyTag<T>(*enc_method, *enc_uri, *enc_iv, *enc_keyformat, |
| *enc_keyformat_versions); |
| }); |
| } |
| |
| } // namespace |
| |
| ParseStatus::Or<XKeyTag> XKeyTag::Parse( |
| TagItem tag, |
| const VariableDictionary& vars, |
| VariableDictionary::SubstitutionBuffer& subs) { |
| DCHECK(tag.GetName() == ToTagName(XKeyTag::kName)); |
| return ParseKeyTag<XKeyTag>(tag, vars, subs); |
| } |
| |
| ParseStatus::Or<XSessionKeyTag> XSessionKeyTag::Parse( |
| TagItem tag, |
| const VariableDictionary& vars, |
| VariableDictionary::SubstitutionBuffer& subs) { |
| DCHECK(tag.GetName() == ToTagName(XSessionKeyTag::kName)); |
| return ParseKeyTag<XSessionKeyTag>(tag, vars, subs); |
| } |
| |
| #undef RETURN_IF_ERROR |
| |
| } // namespace media::hls |