Skip to content

Commit 1e97a9d

Browse files
committed
Updating tags in section 4.4.2 of draft 16
Media or Multivariant Playlist tags are defined in section 4.4.2: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-16#section-4.4.2 This commit updates the existing tags in that section to be in line with draft 16. The main change here is the addition of the EXT-X-DEFINE tag and associated validators (both tag and playlist validators). A further improvement may be to add handling for variable substitution into mamba, as defined in section 4.3: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-16#section-4.3 The difficulty there would be in implementing the IMPORT definition, as it could not be self-contained to one parsed playlist, and would need a way of associating another parsed multivariant playlist, or at least the key-value pairs that were obtained from it.
1 parent 9ec6fcf commit 1e97a9d

File tree

10 files changed

+489
-11
lines changed

10 files changed

+489
-11
lines changed

mamba.xcodeproj/project.pbxproj

+16
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@
5353
6DD9C898242CCE4A00B90E1C /* hls_variant_playlist_with_daterange_metadata.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 6DD9C897242CCE4A00B90E1C /* hls_variant_playlist_with_daterange_metadata.m3u8 */; };
5454
6DD9C899242CCE5300B90E1C /* hls_variant_playlist_with_daterange_metadata.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 6DD9C897242CCE4A00B90E1C /* hls_variant_playlist_with_daterange_metadata.m3u8 */; };
5555
6DD9C89A242CCE5400B90E1C /* hls_variant_playlist_with_daterange_metadata.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 6DD9C897242CCE4A00B90E1C /* hls_variant_playlist_with_daterange_metadata.m3u8 */; };
56+
82E2777E2CFBF92800D6C95D /* EXT_X_DEFINETagValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E2777D2CFBF91C00D6C95D /* EXT_X_DEFINETagValidator.swift */; };
57+
82E2777F2CFBF92800D6C95D /* EXT_X_DEFINETagValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E2777D2CFBF91C00D6C95D /* EXT_X_DEFINETagValidator.swift */; };
58+
82E277802CFBF92800D6C95D /* EXT_X_DEFINETagValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E2777D2CFBF91C00D6C95D /* EXT_X_DEFINETagValidator.swift */; };
59+
82E277822CFC005B00D6C95D /* EXT_X_DEFINEPlaylistValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E277812CFC005600D6C95D /* EXT_X_DEFINEPlaylistValidator.swift */; };
60+
82E277832CFC005B00D6C95D /* EXT_X_DEFINEPlaylistValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E277812CFC005600D6C95D /* EXT_X_DEFINEPlaylistValidator.swift */; };
61+
82E277842CFC005B00D6C95D /* EXT_X_DEFINEPlaylistValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E277812CFC005600D6C95D /* EXT_X_DEFINEPlaylistValidator.swift */; };
5662
883290561EA172170064588B /* HLSStringRefExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 883290551EA172170064588B /* HLSStringRefExtensionTests.swift */; };
5763
D44E03771E3BAC9F00126B52 /* HLSTag+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44E03761E3BAC9F00126B52 /* HLSTag+Util.swift */; };
5864
D44E03781E3BAC9F00126B52 /* HLSTag+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44E03761E3BAC9F00126B52 /* HLSTag+Util.swift */; };
@@ -665,6 +671,8 @@
665671
6DD0A1AC242F85C800FF7AAE /* EXT_X_DATERANGETagValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXT_X_DATERANGETagValidator.swift; sourceTree = "<group>"; };
666672
6DD0A1B0242FADC600FF7AAE /* EXT_X_DATERANGEPlaylistValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXT_X_DATERANGEPlaylistValidator.swift; sourceTree = "<group>"; };
667673
6DD9C897242CCE4A00B90E1C /* hls_variant_playlist_with_daterange_metadata.m3u8 */ = {isa = PBXFileReference; lastKnownFileType = text; path = hls_variant_playlist_with_daterange_metadata.m3u8; sourceTree = "<group>"; };
674+
82E2777D2CFBF91C00D6C95D /* EXT_X_DEFINETagValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXT_X_DEFINETagValidator.swift; sourceTree = "<group>"; };
675+
82E277812CFC005600D6C95D /* EXT_X_DEFINEPlaylistValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXT_X_DEFINEPlaylistValidator.swift; sourceTree = "<group>"; };
668676
883290551EA172170064588B /* HLSStringRefExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HLSStringRefExtensionTests.swift; sourceTree = "<group>"; };
669677
D44E03761E3BAC9F00126B52 /* HLSTag+Util.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HLSTag+Util.swift"; sourceTree = "<group>"; };
670678
D4BB018C1E2EABD500CA006E /* HLSTagArray+RenditionGroups.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HLSTagArray+RenditionGroups.swift"; sourceTree = "<group>"; };
@@ -941,6 +949,8 @@
941949
children = (
942950
6DD0A1B0242FADC600FF7AAE /* EXT_X_DATERANGEPlaylistValidator.swift */,
943951
6DD0A1AC242F85C800FF7AAE /* EXT_X_DATERANGETagValidator.swift */,
952+
82E277812CFC005600D6C95D /* EXT_X_DEFINEPlaylistValidator.swift */,
953+
82E2777D2CFBF91C00D6C95D /* EXT_X_DEFINETagValidator.swift */,
944954
EC3B019E1DD4D47900B512E3 /* EXT_X_KEYValidator.swift */,
945955
EC3B019F1DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupAUTOSELECTValidator.swift */,
946956
EC3B01A01DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupDEFAULTValidator.swift */,
@@ -1809,6 +1819,7 @@
18091819
EC3B01A91DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupDEFAULTValidator.swift in Sources */,
18101820
EC8A3C821F7C497400A50EED /* HLSPlaylistCore.swift in Sources */,
18111821
EC3B01C31DD4D49A00B512E3 /* HLSPlaylistOneToManyValidator.swift in Sources */,
1822+
82E2777E2CFBF92800D6C95D /* EXT_X_DEFINETagValidator.swift in Sources */,
18121823
ECFAA6581E6DD93C00398D66 /* HLSPlaylist.swift in Sources */,
18131824
ECC410601EA02F4800B4E3C8 /* StructureState.swift in Sources */,
18141825
EC7491811DD29C3500AF4E20 /* String+Trim.swift in Sources */,
@@ -1879,6 +1890,7 @@
18791890
EC7491F11DD29DBB00AF4E20 /* LocationTagWriter.swift in Sources */,
18801891
EC7491671DD29B0F00AF4E20 /* RegisteredHLSTags.swift in Sources */,
18811892
EC74914C1DD29ACF00AF4E20 /* HLSTagCriteria.swift in Sources */,
1893+
82E277822CFC005B00D6C95D /* EXT_X_DEFINEPlaylistValidator.swift in Sources */,
18821894
EC95477C1E5CC7C800962535 /* OutputStream+HLSWriting.swift in Sources */,
18831895
EC3B01C71DD4D49A00B512E3 /* HLSPlaylistRenditionGroupMatchingNAMELANGUAGEValidator.swift in Sources */,
18841896
EC74917D1DD29C3500AF4E20 /* String+DateParsing.swift in Sources */,
@@ -1983,6 +1995,7 @@
19831995
43DE4F0E1E564FFE00EEE800 /* EXT_X_MEDIARenditionINSTREAMIDValidator.swift in Sources */,
19841996
EC8A3C831F7C497400A50EED /* HLSPlaylistCore.swift in Sources */,
19851997
43DE4F0D1E564FEE00EEE800 /* EXT_X_STARTTimeOffsetValidator.swift in Sources */,
1998+
82E277802CFBF92800D6C95D /* EXT_X_DEFINETagValidator.swift in Sources */,
19861999
ECFAA6591E6DD93C00398D66 /* HLSPlaylist.swift in Sources */,
19872000
ECC410611EA02F4800B4E3C8 /* StructureState.swift in Sources */,
19882001
EC3B01AA1DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupDEFAULTValidator.swift in Sources */,
@@ -2053,6 +2066,7 @@
20532066
D44E03781E3BAC9F00126B52 /* HLSTag+Util.swift in Sources */,
20542067
EC7491F21DD29DBB00AF4E20 /* LocationTagWriter.swift in Sources */,
20552068
EC7491681DD29B0F00AF4E20 /* RegisteredHLSTags.swift in Sources */,
2069+
82E277842CFC005B00D6C95D /* EXT_X_DEFINEPlaylistValidator.swift in Sources */,
20562070
EC95477D1E5CC7C900962535 /* OutputStream+HLSWriting.swift in Sources */,
20572071
EC74914D1DD29ACF00AF4E20 /* HLSTagCriteria.swift in Sources */,
20582072
EC3B01C81DD4D49A00B512E3 /* HLSPlaylistRenditionGroupMatchingNAMELANGUAGEValidator.swift in Sources */,
@@ -2157,6 +2171,7 @@
21572171
EC1CCD04209A2CF9006B59FF /* HLSStringRef_ConcreteUnownedBytes.m in Sources */,
21582172
EC1CCD56209A2CF9006B59FF /* LocationTagWriter.swift in Sources */,
21592173
EC1CCD00209A2CF9006B59FF /* HLSStringRef_ConcreteNSData.m in Sources */,
2174+
82E2777F2CFBF92800D6C95D /* EXT_X_DEFINETagValidator.swift in Sources */,
21602175
EC1CCD36209A2CF9006B59FF /* URL+hlsplaylist.swift in Sources */,
21612176
EC1CCD32209A2CF9006B59FF /* String+Trim.swift in Sources */,
21622177
EC1CCD46209A2CF9006B59FF /* GenericSingleTagValidator.swift in Sources */,
@@ -2227,6 +2242,7 @@
22272242
EC1CCD2A209A2CF9006B59FF /* HLSTag+Util.swift in Sources */,
22282243
EC1CCD31209A2CF9006B59FF /* String+HLSTypeEquatable.swift in Sources */,
22292244
EC1CCD2D209A2CF9006B59FF /* RegisteredHLSTags.swift in Sources */,
2245+
82E277832CFC005B00D6C95D /* EXT_X_DEFINEPlaylistValidator.swift in Sources */,
22302246
EC1CCD5A209A2CF9006B59FF /* HLSParserError.swift in Sources */,
22312247
EC1CCD47209A2CF9006B59FF /* HLSDictionaryTagValueIdentifier.swift in Sources */,
22322248
EC1CCCF2209A2CF9006B59FF /* HLSTag.swift in Sources */,

mambaSharedFramework/HLSValidationIssue.swift

+6
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ public enum IssueDescription: String {
5858
case EXT_X_STREAM_INFRenditionGroupCLOSEDCAPTIONSValidator = "EXT-X-STREAM-INF - CLOSED-CAPTIONS The value is a quoted-string or an enumerated-string NONE. If the value is a quoted-string, it MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Playlist whose TYPE attribute is CLOSED-CAPTIONS. If it is NONE, all EXT-X-STREAM-INF tags must have this attribute with a value of NONE."
5959
case EXT_X_TARGETDURATIONLengthValidator = "The EXT-X-TARGETDURATION tag specifies the maximum media segment duration. The EXTINF duration of each media segment in the Playlist file MUST be less than or equal to the target duration."
6060
case EXT_X_STARTTimeOffsetValidator = "TIME-OFFSET absolute value should never be longer than the playlist or If the variant does not contain EXT-X-ENDLIST, TIME-OFFSET should not be within 3 target durations from the end."
61+
case EXT_X_DEFINENameWithNoValue = "The VALUE attribute is REQUIRED if the EXT-X-DEFINE tag has a NAME attribute."
62+
case EXT_X_DEFINENoNameNorImportNorQueryparam = "An EXT-X-DEFINE tag MUST contain either a NAME, an IMPORT, or a QUERYPARAM attribute."
63+
case EXT_X_DEFINEMoreThanOneOfNameImportOrQueryParam = "An EXT-X-DEFINE tag MUST only contain one of NAME, IMPORT, or QUERYPARAM attribute."
64+
case EXT_X_DEFINEDuplicateDefinition = "An EXT-X-DEFINE tag MUST NOT specify the same Variable Name as any other EXT-X-DEFINE tag in the same Playlist. Parsers that encounter duplicate Variable Name declarations MUST fail to parse the Playlist."
65+
case EXT_X_DEFINEImportInMultivariant = "EXT-X-DEFINE tags containing the IMPORT attribute MUST NOT occur in Multivariant Playlists; they are only allowed in Media Playlists."
66+
case EXT_X_DEFINENoQueryParameterValue = "If the QUERYPARAM attribute value does not match any query parameter in the URI or the matching parameter has no associated value, the parser MUST fail to parse the Playlist."
6167
case HLSPlaylistRenditionGroupAUDIOValidator = "All members of a group with TYPE=AUDIO MUST use the same audio sample format."
6268
case HLSPlaylistRenditionGroupVIDEOValidator = "All members of a group with TYPE=VIDEO MUST use the same video sample format."
6369
case EXT_X_MEDIARenditionGroupNAMEValidator = "All EXT-X-MEDIA tags in the same group MUST have different NAME attributes."
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
//
2+
// EXT_X_DEFINEPlaylistValidator.swift
3+
// mamba
4+
//
5+
// Created by Robert Galluccio on 11/30/24.
6+
// Copyright © 2024 Comcast Corporation.
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License. All rights reserved.
18+
//
19+
20+
class EXT_X_DEFINEPlaylistValidator: HLSPlaylistValidator {
21+
static func validate(hlsPlaylist: any HLSPlaylistInterface) -> [HLSValidationIssue]? {
22+
let tagValidator = EXT_X_DEFINETagValidator()
23+
var validationIssues = [HLSValidationIssue]()
24+
var variableNames = Set<String>()
25+
26+
for tag in hlsPlaylist.tags where tag.tagDescriptor == PantosTag.EXT_X_DEFINE {
27+
// An EXT-X-DEFINE tag MUST contain either a NAME, an IMPORT, or a QUERYPARAM attribute, but only one of the
28+
// three. Otherwise, the client MUST fail to parse the Playlist.
29+
let fatalTagIssues = (tagValidator.validate(tag: tag) ?? []).filter {
30+
$0.description == IssueDescription.EXT_X_DEFINENoNameNorImportNorQueryparam.rawValue ||
31+
$0.description == IssueDescription.EXT_X_DEFINEMoreThanOneOfNameImportOrQueryParam.rawValue
32+
}
33+
validationIssues.append(contentsOf: fatalTagIssues)
34+
35+
var variableName: String?
36+
switch DefineVariableName(tag: tag) {
37+
case let .name(value):
38+
variableName = value
39+
case let .import(value):
40+
variableName = value
41+
// EXT-X-DEFINE tags containing the IMPORT attribute MUST NOT occur in Multivariant Playlists; they are
42+
// only allowed in Media Playlists.
43+
switch hlsPlaylist.type {
44+
case .master:
45+
validationIssues.append(
46+
HLSValidationIssue(
47+
description: .EXT_X_DEFINEImportInMultivariant,
48+
severity: .error
49+
)
50+
)
51+
case .media, .unknown:
52+
break
53+
}
54+
case let .queryparam(value):
55+
variableName = value
56+
// If the QUERYPARAM attribute value does not match any query parameter in the URI or the matching
57+
// parameter has no associated value, the parser MUST fail to parse the Playlist.
58+
if
59+
let playlist = (hlsPlaylist as? HLSPlaylist),
60+
let urlComponents = URLComponents(url: playlist.url, resolvingAgainstBaseURL: true)
61+
{
62+
let queryItems = urlComponents.queryItems ?? []
63+
if !queryItems.contains(where: { $0.name == value && !($0.value ?? "").isEmpty }) {
64+
validationIssues.append(
65+
HLSValidationIssue(
66+
description: .EXT_X_DEFINENoQueryParameterValue,
67+
severity: .error
68+
)
69+
)
70+
}
71+
}
72+
case .none:
73+
break // This issue will already have been added in the check above.
74+
}
75+
// An EXT-X-DEFINE tag MUST NOT specify the same Variable Name as any other EXT-X-DEFINE tag in the same
76+
// Playlist. Parsers that encounter duplicate Variable Name declarations MUST fail to parse the Playlist.
77+
if let variableName {
78+
if variableNames.contains(variableName) {
79+
validationIssues.append(
80+
HLSValidationIssue(
81+
description: .EXT_X_DEFINEDuplicateDefinition,
82+
severity: .error
83+
)
84+
)
85+
} else {
86+
variableNames.insert(variableName)
87+
}
88+
}
89+
}
90+
91+
return validationIssues.isEmpty ? nil : validationIssues
92+
}
93+
}
94+
95+
extension EXT_X_DEFINEPlaylistValidator {
96+
private enum DefineVariableName {
97+
case name(String)
98+
case `import`(String)
99+
case queryparam(String)
100+
101+
init?(tag: HLSTag) {
102+
if let v = tag.value(forValueIdentifier: PantosValue.name) {
103+
self = .name(v)
104+
} else if let v = tag.value(forValueIdentifier: PantosValue.import) {
105+
self = .import(v)
106+
} else if let v = tag.value(forValueIdentifier: PantosValue.queryparam) {
107+
self = .queryparam(v)
108+
} else {
109+
return nil
110+
}
111+
}
112+
}
113+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//
2+
// EXT_X_DEFINEValidator.swift
3+
// mamba
4+
//
5+
// Created by Robert Galluccio on 11/30/24.
6+
// Copyright © 2024 Comcast Corporation.
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License. All rights reserved.
18+
//
19+
20+
class EXT_X_DEFINETagValidator: HLSTagValidator {
21+
private let genericValidator: GenericDictionaryTagValidator
22+
23+
init() {
24+
genericValidator = GenericDictionaryTagValidator(
25+
tag: PantosTag.EXT_X_DEFINE,
26+
dictionaryValueIdentifiers: [
27+
HLSDictionaryTagValueIdentifierImpl(
28+
valueId: PantosValue.name,
29+
optional: true,
30+
expectedType: String.self
31+
),
32+
HLSDictionaryTagValueIdentifierImpl(
33+
valueId: PantosValue.value,
34+
optional: true,
35+
expectedType: String.self
36+
),
37+
HLSDictionaryTagValueIdentifierImpl(
38+
valueId: PantosValue.import,
39+
optional: true,
40+
expectedType: String.self
41+
),
42+
HLSDictionaryTagValueIdentifierImpl(
43+
valueId: PantosValue.queryparam,
44+
optional: true,
45+
expectedType: String.self
46+
)
47+
]
48+
)
49+
}
50+
51+
func validate(tag: HLSTag) -> [HLSValidationIssue]? {
52+
var validationIssues = [HLSValidationIssue]()
53+
54+
// An EXT-X-DEFINE tag MUST contain either a NAME, an IMPORT, or a QUERYPARAM attribute, but only one of the
55+
// three.
56+
switch (value(tag, .name), value(tag, .import), value(tag, .queryparam)) {
57+
// Split out NAME separately as we need to validate further that VALUE is present
58+
case (.some, .none, .none):
59+
// [The VALUE] attribute is REQUIRED if the EXT-X-DEFINE tag has a NAME attribute.
60+
if tag.value(forValueIdentifier: PantosValue.value) == nil {
61+
validationIssues.append(HLSValidationIssue(description: .EXT_X_DEFINENameWithNoValue, severity: .error))
62+
}
63+
// There is an issue if none of NAME, IMPORT, nor QUERYPARAM are present
64+
case (.none, .none, .none):
65+
validationIssues.append(
66+
HLSValidationIssue(
67+
description: .EXT_X_DEFINENoNameNorImportNorQueryparam,
68+
severity: .error
69+
)
70+
)
71+
// There is an issue if more than one of NAME, IMPORT, or QUERYPARAM are present
72+
case (.some, .some, _), (_, .some, .some), (.some, _, .some):
73+
validationIssues.append(
74+
HLSValidationIssue(
75+
description: .EXT_X_DEFINEMoreThanOneOfNameImportOrQueryParam,
76+
severity: .error
77+
)
78+
)
79+
// No issues with either only IMPORT or only QUERYPARAM
80+
case (.none, .some, .none), (.none, .none, .some):
81+
break
82+
}
83+
84+
return validationIssues.isEmpty ? nil : validationIssues
85+
}
86+
87+
/// Convenience method for getting a `PantosValue`
88+
///
89+
/// Using this method to allow for the switch statement above to be more concise.
90+
/// - Parameter pantosValue: Value to search for
91+
/// - Returns: String value if it exists
92+
private func value(_ tag: HLSTag, _ pantosValue: PantosValue) -> String? {
93+
tag.value(forValueIdentifier: pantosValue)
94+
}
95+
}

0 commit comments

Comments
 (0)