diff --git a/mamba.xcodeproj/project.pbxproj b/mamba.xcodeproj/project.pbxproj index 31f1514..9758c5c 100644 --- a/mamba.xcodeproj/project.pbxproj +++ b/mamba.xcodeproj/project.pbxproj @@ -61,6 +61,30 @@ E6841B382A0A92C00074DBCC /* version.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2BB693D7276A13A000FE56B1 /* version.txt */; }; E6841B392A0A92C10074DBCC /* version.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2BB693D7276A13A000FE56B1 /* version.txt */; }; E6841B3A2A0A92C10074DBCC /* version.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2BB693D7276A13A000FE56B1 /* version.txt */; }; + E6B2D7F92CC834DB00D319DF /* NoOpTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D7F72CC834DB00D319DF /* NoOpTagParser.swift */; }; + E6B2D7FA2CC834DB00D319DF /* GenericNoDataTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D7F52CC834DB00D319DF /* GenericNoDataTagParser.swift */; }; + E6B2D7FB2CC834DB00D319DF /* GenericDictionaryTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D7F42CC834DB00D319DF /* GenericDictionaryTagParser.swift */; }; + E6B2D7FC2CC834DB00D319DF /* GenericSingleValueTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D7F62CC834DB00D319DF /* GenericSingleValueTagParser.swift */; }; + E6B2D7FD2CC834DB00D319DF /* NoOpTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D7F72CC834DB00D319DF /* NoOpTagParser.swift */; }; + E6B2D7FE2CC834DB00D319DF /* GenericNoDataTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D7F52CC834DB00D319DF /* GenericNoDataTagParser.swift */; }; + E6B2D7FF2CC834DB00D319DF /* GenericDictionaryTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D7F42CC834DB00D319DF /* GenericDictionaryTagParser.swift */; }; + E6B2D8002CC834DB00D319DF /* GenericSingleValueTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D7F62CC834DB00D319DF /* GenericSingleValueTagParser.swift */; }; + E6B2D8012CC834DB00D319DF /* NoOpTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D7F72CC834DB00D319DF /* NoOpTagParser.swift */; }; + E6B2D8022CC834DB00D319DF /* GenericNoDataTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D7F52CC834DB00D319DF /* GenericNoDataTagParser.swift */; }; + E6B2D8032CC834DB00D319DF /* GenericDictionaryTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D7F42CC834DB00D319DF /* GenericDictionaryTagParser.swift */; }; + E6B2D8042CC834DB00D319DF /* GenericSingleValueTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D7F62CC834DB00D319DF /* GenericSingleValueTagParser.swift */; }; + E6B2D8062CC834F500D319DF /* HLSInterstitialValueTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D8052CC834F500D319DF /* HLSInterstitialValueTypes.swift */; }; + E6B2D8072CC834F500D319DF /* HLSInterstitialValueTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D8052CC834F500D319DF /* HLSInterstitialValueTypes.swift */; }; + E6B2D8082CC834F500D319DF /* HLSInterstitialValueTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D8052CC834F500D319DF /* HLSInterstitialValueTypes.swift */; }; + E6B2D80A2CC83E5B00D319DF /* HLSInterstitialValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D8092CC83E5B00D319DF /* HLSInterstitialValueTests.swift */; }; + E6B2D80B2CC83E5B00D319DF /* HLSInterstitialValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D8092CC83E5B00D319DF /* HLSInterstitialValueTests.swift */; }; + E6B2D80C2CC83E5B00D319DF /* HLSInterstitialValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D8092CC83E5B00D319DF /* HLSInterstitialValueTests.swift */; }; + E6B2D80E2CC842DB00D319DF /* InterstitialTagBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D80D2CC842DB00D319DF /* InterstitialTagBuilder.swift */; }; + E6B2D80F2CC842DB00D319DF /* InterstitialTagBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D80D2CC842DB00D319DF /* InterstitialTagBuilder.swift */; }; + E6B2D8102CC842DB00D319DF /* InterstitialTagBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D80D2CC842DB00D319DF /* InterstitialTagBuilder.swift */; }; + E6B2D8122CC937D400D319DF /* InterstitialTagBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D8112CC937D400D319DF /* InterstitialTagBuilderTests.swift */; }; + E6B2D8132CC937D400D319DF /* InterstitialTagBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D8112CC937D400D319DF /* InterstitialTagBuilderTests.swift */; }; + E6B2D8142CC937D400D319DF /* InterstitialTagBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B2D8112CC937D400D319DF /* InterstitialTagBuilderTests.swift */; }; EC03B63D1E5CC55800BF1F97 /* RapidParserMasterParseArray.c in Sources */ = {isa = PBXBuildFile; fileRef = EC03B63B1E5CC55800BF1F97 /* RapidParserMasterParseArray.c */; }; EC03B63E1E5CC55800BF1F97 /* RapidParserMasterParseArray.c in Sources */ = {isa = PBXBuildFile; fileRef = EC03B63B1E5CC55800BF1F97 /* RapidParserMasterParseArray.c */; }; EC03B63F1E5CC55800BF1F97 /* RapidParserMasterParseArray.h in Headers */ = {isa = PBXBuildFile; fileRef = EC03B63C1E5CC55800BF1F97 /* RapidParserMasterParseArray.h */; }; @@ -186,10 +210,6 @@ EC1CCD34209A2CF9006B59FF /* StringArrayParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491841DD29CCB00AF4E20 /* StringArrayParser.swift */; }; EC1CCD35209A2CF9006B59FF /* StringDictionaryParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491851DD29CCB00AF4E20 /* StringDictionaryParser.swift */; }; EC1CCD36209A2CF9006B59FF /* URL+hlsplaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9826011DD3A113003BCDA5 /* URL+hlsplaylist.swift */; }; - EC1CCD37209A2CF9006B59FF /* GenericDictionaryTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491D21DD29D9600AF4E20 /* GenericDictionaryTagParser.swift */; }; - EC1CCD38209A2CF9006B59FF /* GenericNoDataTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491D31DD29D9600AF4E20 /* GenericNoDataTagParser.swift */; }; - EC1CCD39209A2CF9006B59FF /* GenericSingleValueTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491D41DD29D9600AF4E20 /* GenericSingleValueTagParser.swift */; }; - EC1CCD3A209A2CF9006B59FF /* NoOpTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9547841E5CC83C00962535 /* NoOpTagParser.swift */; }; EC1CCD3B209A2CF9006B59FF /* EXTINFValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC95478A1E5CC86300962535 /* EXTINFValidator.swift */; }; EC1CCD3C209A2CF9006B59FF /* EXT_X_KEYValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3B019E1DD4D47900B512E3 /* EXT_X_KEYValidator.swift */; }; EC1CCD3D209A2CF9006B59FF /* EXT_X_MEDIARenditionGroupAUTOSELECTValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3B019F1DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupAUTOSELECTValidator.swift */; }; @@ -345,12 +365,6 @@ EC7491CE1DD29D7C00AF4E20 /* PantosTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491CB1DD29D7C00AF4E20 /* PantosTag.swift */; }; EC7491CF1DD29D7C00AF4E20 /* PantosValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491CC1DD29D7C00AF4E20 /* PantosValue.swift */; }; EC7491D01DD29D7C00AF4E20 /* PantosValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491CC1DD29D7C00AF4E20 /* PantosValue.swift */; }; - EC7491D81DD29D9600AF4E20 /* GenericDictionaryTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491D21DD29D9600AF4E20 /* GenericDictionaryTagParser.swift */; }; - EC7491D91DD29D9600AF4E20 /* GenericDictionaryTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491D21DD29D9600AF4E20 /* GenericDictionaryTagParser.swift */; }; - EC7491DA1DD29D9600AF4E20 /* GenericNoDataTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491D31DD29D9600AF4E20 /* GenericNoDataTagParser.swift */; }; - EC7491DB1DD29D9600AF4E20 /* GenericNoDataTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491D31DD29D9600AF4E20 /* GenericNoDataTagParser.swift */; }; - EC7491DC1DD29D9600AF4E20 /* GenericSingleValueTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491D41DD29D9600AF4E20 /* GenericSingleValueTagParser.swift */; }; - EC7491DD1DD29D9600AF4E20 /* GenericSingleValueTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491D41DD29D9600AF4E20 /* GenericSingleValueTagParser.swift */; }; EC7491EB1DD29DBB00AF4E20 /* GenericDictionaryTagWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491E21DD29DBB00AF4E20 /* GenericDictionaryTagWriter.swift */; }; EC7491EC1DD29DBB00AF4E20 /* GenericDictionaryTagWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491E21DD29DBB00AF4E20 /* GenericDictionaryTagWriter.swift */; }; EC7491EF1DD29DBB00AF4E20 /* GenericSingleTagWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7491E41DD29DBB00AF4E20 /* GenericSingleTagWriter.swift */; }; @@ -464,8 +478,6 @@ EC9547801E5CC80800962535 /* HLSStringRef+mamba.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC95477E1E5CC80800962535 /* HLSStringRef+mamba.swift */; }; EC9547821E5CC82500962535 /* GenericTagWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9547811E5CC82500962535 /* GenericTagWriter.swift */; }; EC9547831E5CC82500962535 /* GenericTagWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9547811E5CC82500962535 /* GenericTagWriter.swift */; }; - EC9547851E5CC83C00962535 /* NoOpTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9547841E5CC83C00962535 /* NoOpTagParser.swift */; }; - EC9547861E5CC83C00962535 /* NoOpTagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9547841E5CC83C00962535 /* NoOpTagParser.swift */; }; EC9547881E5CC84700962535 /* FrameworkInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9547871E5CC84700962535 /* FrameworkInfo.swift */; }; EC9547891E5CC84700962535 /* FrameworkInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9547871E5CC84700962535 /* FrameworkInfo.swift */; }; EC95478B1E5CC86300962535 /* EXTINFValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC95478A1E5CC86300962535 /* EXTINFValidator.swift */; }; @@ -656,6 +668,14 @@ 883290551EA172170064588B /* HLSStringRefExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HLSStringRefExtensionTests.swift; sourceTree = ""; }; D44E03761E3BAC9F00126B52 /* HLSTag+Util.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HLSTag+Util.swift"; sourceTree = ""; }; D4BB018C1E2EABD500CA006E /* HLSTagArray+RenditionGroups.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HLSTagArray+RenditionGroups.swift"; sourceTree = ""; }; + E6B2D7F42CC834DB00D319DF /* GenericDictionaryTagParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericDictionaryTagParser.swift; sourceTree = ""; }; + E6B2D7F52CC834DB00D319DF /* GenericNoDataTagParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericNoDataTagParser.swift; sourceTree = ""; }; + E6B2D7F62CC834DB00D319DF /* GenericSingleValueTagParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericSingleValueTagParser.swift; sourceTree = ""; }; + E6B2D7F72CC834DB00D319DF /* NoOpTagParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoOpTagParser.swift; sourceTree = ""; }; + E6B2D8052CC834F500D319DF /* HLSInterstitialValueTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HLSInterstitialValueTypes.swift; sourceTree = ""; }; + E6B2D8092CC83E5B00D319DF /* HLSInterstitialValueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HLSInterstitialValueTests.swift; sourceTree = ""; }; + E6B2D80D2CC842DB00D319DF /* InterstitialTagBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterstitialTagBuilder.swift; sourceTree = ""; }; + E6B2D8112CC937D400D319DF /* InterstitialTagBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterstitialTagBuilderTests.swift; sourceTree = ""; }; EC02E9761E26E77900D4CEAC /* OHHTTPStubs.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OHHTTPStubs.framework; path = Carthage/Build/tvOS/OHHTTPStubs.framework; sourceTree = ""; }; EC03B62D1E5CC54900BF1F97 /* PrototypeRapidParseArray.include */ = {isa = PBXFileReference; lastKnownFileType = text; path = PrototypeRapidParseArray.include; sourceTree = ""; }; EC03B62E1E5CC54900BF1F97 /* RapidParser_LookingForEForEXTINFState_ParseArray.include */ = {isa = PBXFileReference; lastKnownFileType = text; path = RapidParser_LookingForEForEXTINFState_ParseArray.include; sourceTree = ""; }; @@ -769,9 +789,6 @@ EC7491B21DD29D5C00AF4E20 /* HLSWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HLSWriter.swift; sourceTree = ""; }; EC7491CB1DD29D7C00AF4E20 /* PantosTag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PantosTag.swift; sourceTree = ""; }; EC7491CC1DD29D7C00AF4E20 /* PantosValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PantosValue.swift; sourceTree = ""; }; - EC7491D21DD29D9600AF4E20 /* GenericDictionaryTagParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericDictionaryTagParser.swift; sourceTree = ""; }; - EC7491D31DD29D9600AF4E20 /* GenericNoDataTagParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericNoDataTagParser.swift; sourceTree = ""; }; - EC7491D41DD29D9600AF4E20 /* GenericSingleValueTagParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericSingleValueTagParser.swift; sourceTree = ""; }; EC7491E21DD29DBB00AF4E20 /* GenericDictionaryTagWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericDictionaryTagWriter.swift; sourceTree = ""; }; EC7491E41DD29DBB00AF4E20 /* GenericSingleTagWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericSingleTagWriter.swift; sourceTree = ""; }; EC7491E51DD29DBB00AF4E20 /* LocationTagWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationTagWriter.swift; sourceTree = ""; }; @@ -828,7 +845,6 @@ EC95477B1E5CC7C800962535 /* OutputStream+HLSWriting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OutputStream+HLSWriting.swift"; sourceTree = ""; }; EC95477E1E5CC80800962535 /* HLSStringRef+mamba.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HLSStringRef+mamba.swift"; sourceTree = ""; }; EC9547811E5CC82500962535 /* GenericTagWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericTagWriter.swift; sourceTree = ""; }; - EC9547841E5CC83C00962535 /* NoOpTagParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoOpTagParser.swift; sourceTree = ""; }; EC9547871E5CC84700962535 /* FrameworkInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FrameworkInfo.swift; sourceTree = ""; }; EC95478A1E5CC86300962535 /* EXTINFValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EXTINFValidator.swift; sourceTree = ""; }; EC9826011DD3A113003BCDA5 /* URL+hlsplaylist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+hlsplaylist.swift"; sourceTree = ""; }; @@ -973,6 +989,17 @@ path = Resources; sourceTree = ""; }; + E6B2D7F82CC834DB00D319DF /* Pantos-Generic Tag Parsers */ = { + isa = PBXGroup; + children = ( + E6B2D7F42CC834DB00D319DF /* GenericDictionaryTagParser.swift */, + E6B2D7F52CC834DB00D319DF /* GenericNoDataTagParser.swift */, + E6B2D7F62CC834DB00D319DF /* GenericSingleValueTagParser.swift */, + E6B2D7F72CC834DB00D319DF /* NoOpTagParser.swift */, + ); + path = "Pantos-Generic Tag Parsers"; + sourceTree = ""; + }; EC03B62B1E5CC51C00BF1F97 /* HLS Rapid Parser */ = { isa = PBXGroup; children = ( @@ -1107,8 +1134,10 @@ EC7491AF1DD29D5C00AF4E20 /* HLSValidationIssue.swift */, EC7491B11DD29D5C00AF4E20 /* HLSValueTypes.swift */, EC7491B21DD29D5C00AF4E20 /* HLSWriter.swift */, + E6B2D8052CC834F500D319DF /* HLSInterstitialValueTypes.swift */, EC9547871E5CC84700962535 /* FrameworkInfo.swift */, EC1521511DD28536006FB265 /* mamba.h */, + E6B2D80D2CC842DB00D319DF /* InterstitialTagBuilder.swift */, ); path = mambaSharedFramework; sourceTree = ""; @@ -1279,17 +1308,6 @@ path = Helpers; sourceTree = ""; }; - EC7D1C791D343B5A007F971D /* Pantos-Generic Tag Parsers */ = { - isa = PBXGroup; - children = ( - EC7491D21DD29D9600AF4E20 /* GenericDictionaryTagParser.swift */, - EC7491D31DD29D9600AF4E20 /* GenericNoDataTagParser.swift */, - EC7491D41DD29D9600AF4E20 /* GenericSingleValueTagParser.swift */, - EC9547841E5CC83C00962535 /* NoOpTagParser.swift */, - ); - path = "Pantos-Generic Tag Parsers"; - sourceTree = ""; - }; EC7ECA011D30177A000EEB7D /* HLS Utils */ = { isa = PBXGroup; children = ( @@ -1329,6 +1347,7 @@ EC073F5F1FE08F7500689228 /* String+Helio.swift */, EC7492A61DD29F7000AF4E20 /* URL+hlsplaylistTests.swift */, EC9BCAA21D749D8B0032BEBE /* Value Types */, + E6B2D8112CC937D400D319DF /* InterstitialTagBuilderTests.swift */, ); path = "Util Tests"; sourceTree = ""; @@ -1342,6 +1361,7 @@ EC7492B11DD29F8900AF4E20 /* HLSPlaylistTypeTests.swift */, EC7492B21DD29F8900AF4E20 /* HLSResolutionTests.swift */, 1447583C2C8693E000D12CCD /* HLSVideoLayoutTests.swift */, + E6B2D8092CC83E5B00D319DF /* HLSInterstitialValueTests.swift */, ); path = "Value Types"; sourceTree = ""; @@ -1349,7 +1369,7 @@ ECBE47001D33F4100081D096 /* Pantos-Generic HLS Playlist Parsing */ = { isa = PBXGroup; children = ( - EC7D1C791D343B5A007F971D /* Pantos-Generic Tag Parsers */, + E6B2D7F82CC834DB00D319DF /* Pantos-Generic Tag Parsers */, 0173AB0D1D5BB371005DE51B /* Pantos-Generic Tag Validators */, EC550EEA1D35A71700706DC9 /* Pantos-Generic Tag Writers */, EC7491CB1DD29D7C00AF4E20 /* PantosTag.swift */, @@ -1795,13 +1815,12 @@ 144758352C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */, EC7491C31DD29D5C00AF4E20 /* HLSValidationIssue.swift in Sources */, EC74916E1DD29B5D00AF4E20 /* CollectionType+FindExtensions.swift in Sources */, - EC7491DA1DD29D9600AF4E20 /* GenericNoDataTagParser.swift in Sources */, + E6B2D8072CC834F500D319DF /* HLSInterstitialValueTypes.swift in Sources */, EC7491C91DD29D5C00AF4E20 /* HLSWriter.swift in Sources */, EC3B01A51DD4D47900B512E3 /* EXT_X_KEYValidator.swift in Sources */, EC7491721DD29B5D00AF4E20 /* OrderedDictionary.swift in Sources */, EC3B01BF1DD4D49A00B512E3 /* HLSPlaylistCardinalityValidator.swift in Sources */, EC7491FA1DD29DD300AF4E20 /* GenericSingleTagValidator.swift in Sources */, - EC9547851E5CC83C00962535 /* NoOpTagParser.swift in Sources */, EC42A5F21FD9B88E00317EA5 /* IndeterminateBool.swift in Sources */, EC74914E1DD29ACF00AF4E20 /* HLSTagCriterion.swift in Sources */, EC4424891E95A69C00AECFAB /* HLSPlaylistStructure.swift in Sources */, @@ -1826,6 +1845,10 @@ EC03B6541E5CC56B00BF1F97 /* HLSRapidParser.m in Sources */, F70E9E9A1E8C43C8006022C6 /* HLSParserError.swift in Sources */, EC7491701DD29B5D00AF4E20 /* CollectionType+Safe.swift in Sources */, + E6B2D7FD2CC834DB00D319DF /* NoOpTagParser.swift in Sources */, + E6B2D7FE2CC834DB00D319DF /* GenericNoDataTagParser.swift in Sources */, + E6B2D7FF2CC834DB00D319DF /* GenericDictionaryTagParser.swift in Sources */, + E6B2D8002CC834DB00D319DF /* GenericSingleValueTagParser.swift in Sources */, EC7491BB1DD29D5C00AF4E20 /* HLSTagParser.swift in Sources */, EC7491C11DD29D5C00AF4E20 /* HLSTagWriter.swift in Sources */, EC3B01CD1DD4D49A00B512E3 /* HLSPlaylistTagCardinalityValidation.swift in Sources */, @@ -1869,13 +1892,12 @@ 43DE4EFB1E564DA300EEE800 /* EXT_X_STARTTimeOffsetValidator.swift in Sources */, EC74918A1DD29CCB00AF4E20 /* StringDictionaryParser.swift in Sources */, F73183771E78758B00ED8E59 /* HLSStringRefFactory.m in Sources */, + E6B2D8102CC842DB00D319DF /* InterstitialTagBuilder.swift in Sources */, D4BB018D1E2EABD500CA006E /* HLSTagArray+RenditionGroups.swift in Sources */, EC7491881DD29CCB00AF4E20 /* StringArrayParser.swift in Sources */, - EC7491D81DD29D9600AF4E20 /* GenericDictionaryTagParser.swift in Sources */, EC03B6681E5CC56B00BF1F97 /* RapidParserLineState.c in Sources */, EC7491EB1DD29DBB00AF4E20 /* GenericDictionaryTagWriter.swift in Sources */, EC03B6641E5CC56B00BF1F97 /* RapidParserError.m in Sources */, - EC7491DC1DD29D9600AF4E20 /* GenericSingleValueTagParser.swift in Sources */, EC7491CF1DD29D7C00AF4E20 /* PantosValue.swift in Sources */, EC7491CD1DD29D7C00AF4E20 /* PantosTag.swift in Sources */, EC9547881E5CC84700962535 /* FrameworkInfo.swift in Sources */, @@ -1897,6 +1919,7 @@ EC7492741DD29EC800AF4E20 /* EXT_X_I_FRAME_STREAM_INFTagParserTests.swift in Sources */, ECFBD90E1E5CCC2200379FC2 /* HLSStringRefTests.m in Sources */, EC7492401DD29E7300AF4E20 /* HLSParser_Super8MuxedTests.swift in Sources */, + E6B2D8122CC937D400D319DF /* InterstitialTagBuilderTests.swift in Sources */, 4367C3731EAE83C000685945 /* HLSPlaylistStructureMasterTests.swift in Sources */, EC74922E1DD29E4A00AF4E20 /* HLSPlaylistMock.swift in Sources */, EC7492611DD29E9A00AF4E20 /* HLSTagWriting.swift in Sources */, @@ -1939,6 +1962,7 @@ EC7492B71DD29F8900AF4E20 /* HLSPlaylistTypeTests.swift in Sources */, ECFBD9101E5CCC2200379FC2 /* ParseArrayTests.m in Sources */, EC7492B31DD29F8900AF4E20 /* HLSCodecArrayTests.swift in Sources */, + E6B2D80C2CC83E5B00D319DF /* HLSInterstitialValueTests.swift in Sources */, 43DE4EFF1E564E1500EEE800 /* HLSMediaSpanTests.swift in Sources */, ECE36DE41F2A9F10005E5DA7 /* HLSPlaylistTimelineAndSequencingTests.swift in Sources */, 01CD2E7A1DE4D46F002510E7 /* EXT_X_MAPTagParserTests.swift in Sources */, @@ -1965,15 +1989,14 @@ 144758362C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */, EC3B01C41DD4D49A00B512E3 /* HLSPlaylistOneToManyValidator.swift in Sources */, EC7491821DD29C3500AF4E20 /* String+Trim.swift in Sources */, + E6B2D8082CC834F500D319DF /* HLSInterstitialValueTypes.swift in Sources */, EC7491C41DD29D5C00AF4E20 /* HLSValidationIssue.swift in Sources */, EC74916F1DD29B5D00AF4E20 /* CollectionType+FindExtensions.swift in Sources */, - EC7491DB1DD29D9600AF4E20 /* GenericNoDataTagParser.swift in Sources */, EC7491CA1DD29D5C00AF4E20 /* HLSWriter.swift in Sources */, EC3B01A61DD4D47900B512E3 /* EXT_X_KEYValidator.swift in Sources */, EC7491731DD29B5D00AF4E20 /* OrderedDictionary.swift in Sources */, EC3B01C01DD4D49A00B512E3 /* HLSPlaylistCardinalityValidator.swift in Sources */, EC42A5F31FD9B88E00317EA5 /* IndeterminateBool.swift in Sources */, - EC9547861E5CC83C00962535 /* NoOpTagParser.swift in Sources */, EC7491FB1DD29DD300AF4E20 /* GenericSingleTagValidator.swift in Sources */, EC44248A1E95A69C00AECFAB /* HLSPlaylistStructure.swift in Sources */, EC74914F1DD29ACF00AF4E20 /* HLSTagCriterion.swift in Sources */, @@ -1996,6 +2019,10 @@ EC7491B61DD29D5C00AF4E20 /* HLSParser.swift in Sources */, F70E9E9B1E8C43C8006022C6 /* HLSParserError.swift in Sources */, EC03B6551E5CC56B00BF1F97 /* HLSRapidParser.m in Sources */, + E6B2D7F92CC834DB00D319DF /* NoOpTagParser.swift in Sources */, + E6B2D7FA2CC834DB00D319DF /* GenericNoDataTagParser.swift in Sources */, + E6B2D7FB2CC834DB00D319DF /* GenericDictionaryTagParser.swift in Sources */, + E6B2D7FC2CC834DB00D319DF /* GenericSingleValueTagParser.swift in Sources */, 6DD0A1AE242F85C800FF7AAE /* EXT_X_DATERANGETagValidator.swift in Sources */, 6DD0A1B2242FADC600FF7AAE /* EXT_X_DATERANGEPlaylistValidator.swift in Sources */, EC7491711DD29B5D00AF4E20 /* CollectionType+Safe.swift in Sources */, @@ -2039,13 +2066,12 @@ EC3B01A81DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupAUTOSELECTValidator.swift in Sources */, EC74918B1DD29CCB00AF4E20 /* StringDictionaryParser.swift in Sources */, F73183781E78758B00ED8E59 /* HLSStringRefFactory.m in Sources */, + E6B2D80E2CC842DB00D319DF /* InterstitialTagBuilder.swift in Sources */, D4BB018E1E2EABD500CA006E /* HLSTagArray+RenditionGroups.swift in Sources */, EC7491891DD29CCB00AF4E20 /* StringArrayParser.swift in Sources */, - EC7491D91DD29D9600AF4E20 /* GenericDictionaryTagParser.swift in Sources */, EC03B6691E5CC56B00BF1F97 /* RapidParserLineState.c in Sources */, EC7491EC1DD29DBB00AF4E20 /* GenericDictionaryTagWriter.swift in Sources */, EC03B6651E5CC56B00BF1F97 /* RapidParserError.m in Sources */, - EC7491DD1DD29D9600AF4E20 /* GenericSingleValueTagParser.swift in Sources */, EC7491D01DD29D7C00AF4E20 /* PantosValue.swift in Sources */, EC7491CE1DD29D7C00AF4E20 /* PantosTag.swift in Sources */, EC9547891E5CC84700962535 /* FrameworkInfo.swift in Sources */, @@ -2067,6 +2093,7 @@ EC7492AA1DD29F7000AF4E20 /* MambaUtilTests.swift in Sources */, ECFBD90F1E5CCC2200379FC2 /* HLSStringRefTests.m in Sources */, EC7492751DD29EC800AF4E20 /* EXT_X_I_FRAME_STREAM_INFTagParserTests.swift in Sources */, + E6B2D8132CC937D400D319DF /* InterstitialTagBuilderTests.swift in Sources */, EC7492411DD29E7300AF4E20 /* HLSParser_Super8MuxedTests.swift in Sources */, EC74922F1DD29E4A00AF4E20 /* HLSPlaylistMock.swift in Sources */, EC7492621DD29E9A00AF4E20 /* HLSTagWriting.swift in Sources */, @@ -2109,6 +2136,7 @@ EC74922D1DD29E4A00AF4E20 /* HLSPlaylist+Convenience.swift in Sources */, ECFBD9111E5CCC2200379FC2 /* ParseArrayTests.m in Sources */, EC7492B81DD29F8900AF4E20 /* HLSPlaylistTypeTests.swift in Sources */, + E6B2D80B2CC83E5B00D319DF /* HLSInterstitialValueTests.swift in Sources */, 43DE4F001E564E1500EEE800 /* HLSMediaSpanTests.swift in Sources */, EC7492B41DD29F8900AF4E20 /* HLSCodecArrayTests.swift in Sources */, ECE36DE51F2A9F10005E5DA7 /* HLSPlaylistTimelineAndSequencingTests.swift in Sources */, @@ -2135,6 +2163,7 @@ 144758372C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */, EC1CCD59209A2CF9006B59FF /* HLSParser.swift in Sources */, EC1CCD30209A2CF9006B59FF /* String+DateParsing.swift in Sources */, + E6B2D8062CC834F500D319DF /* HLSInterstitialValueTypes.swift in Sources */, EC1CCD53209A2CF9006B59FF /* GenericDictionaryTagWriter.swift in Sources */, EC1CCD55209A2CF9006B59FF /* GenericTagWriter.swift in Sources */, EC1CCD60209A2CF9006B59FF /* HLSValidationIssue.swift in Sources */, @@ -2160,13 +2189,15 @@ EC1CCD50209A2CF9006B59FF /* HLSPlaylistTagGroupValidator.swift in Sources */, EC1CCCFE209A2CF9006B59FF /* HLSStringRefFactory.m in Sources */, 6DD0A1B3242FADC600FF7AAE /* EXT_X_DATERANGEPlaylistValidator.swift in Sources */, - EC1CCD3A209A2CF9006B59FF /* NoOpTagParser.swift in Sources */, EC1CCD5D209A2CF9006B59FF /* HLSTagValidator.swift in Sources */, EC1CCD62209A2CF9006B59FF /* HLSWriter.swift in Sources */, EC1CCCF8209A2CF9006B59FF /* HLSTagCriteria.swift in Sources */, EC1CCCF4209A2CF9006B59FF /* HLSPlaylistStructureInterface.swift in Sources */, + E6B2D8012CC834DB00D319DF /* NoOpTagParser.swift in Sources */, + E6B2D8022CC834DB00D319DF /* GenericNoDataTagParser.swift in Sources */, + E6B2D8032CC834DB00D319DF /* GenericDictionaryTagParser.swift in Sources */, + E6B2D8042CC834DB00D319DF /* GenericSingleValueTagParser.swift in Sources */, EC1CCD61209A2CF9006B59FF /* HLSValueTypes.swift in Sources */, - EC1CCD39209A2CF9006B59FF /* GenericSingleValueTagParser.swift in Sources */, EC1CCD34209A2CF9006B59FF /* StringArrayParser.swift in Sources */, EC1CCD18209A2CF9006B59FF /* RapidParser.c in Sources */, EC1CCD3E209A2CF9006B59FF /* EXT_X_MEDIARenditionGroupDEFAULTValidator.swift in Sources */, @@ -2207,11 +2238,10 @@ 1447582F2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift in Sources */, EC1CCD35209A2CF9006B59FF /* StringDictionaryParser.swift in Sources */, EC1CCD02209A2CF9006B59FF /* HLSStringRef_ConcreteNSString.m in Sources */, - EC1CCD38209A2CF9006B59FF /* GenericNoDataTagParser.swift in Sources */, EC1CCD51209A2CF9006B59FF /* HLSPlaylistValidator.swift in Sources */, EC1CCD5F209A2CF9006B59FF /* HLSTagWriter.swift in Sources */, + E6B2D80F2CC842DB00D319DF /* InterstitialTagBuilder.swift in Sources */, EC1CCD49209A2CF9006B59FF /* HLSPlaylistCollectionValidator.swift in Sources */, - EC1CCD37209A2CF9006B59FF /* GenericDictionaryTagParser.swift in Sources */, EC1CCD28209A2CF9006B59FF /* FailableStringLiteralConvertible.swift in Sources */, EC1CCCF7209A2CF9006B59FF /* StructureState.swift in Sources */, EC1CCD5C209A2CF9006B59FF /* HLSTagParser.swift in Sources */, @@ -2237,6 +2267,7 @@ ECE253EA209A50A100D388CE /* HLSStringRefTests.m in Sources */, ECE253F7209A50B500D388CE /* StringArrayParserTests.swift in Sources */, ECE25401209A50B500D388CE /* MambaUtilTests.swift in Sources */, + E6B2D8142CC937D400D319DF /* InterstitialTagBuilderTests.swift in Sources */, ECE253FB209A50B500D388CE /* GenericDictionaryTagWriterTests.swift in Sources */, ECE25407209A50B500D388CE /* HLSPlaylistTypeTests.swift in Sources */, ECE253D5209A509000D388CE /* XCTestCase+mamba.swift in Sources */, @@ -2279,6 +2310,7 @@ ECE253FC209A50B500D388CE /* GenericSingleTagWriterTests.swift in Sources */, ECE253DA209A509900D388CE /* HLSPlaylistTests.swift in Sources */, ECE253F4209A50B500D388CE /* EXT_X_PROGRAM_DATE_TIMEParserTests.swift in Sources */, + E6B2D80A2CC83E5B00D319DF /* HLSInterstitialValueTests.swift in Sources */, ECE253FD209A50B500D388CE /* ThirdPartyTagListSupportTests.swift in Sources */, ECE25408209A50B500D388CE /* HLSResolutionTests.swift in Sources */, ECE253F6209A50B500D388CE /* GenericSingleValueTagParserTests.swift in Sources */, diff --git a/mambaSharedFramework/HLS Utils/String Util/String+DateParsing.swift b/mambaSharedFramework/HLS Utils/String Util/String+DateParsing.swift index 4681469..a80ace7 100644 --- a/mambaSharedFramework/HLS Utils/String Util/String+DateParsing.swift +++ b/mambaSharedFramework/HLS Utils/String Util/String+DateParsing.swift @@ -21,7 +21,7 @@ import Foundation extension String { - private struct DateFormatter { + struct DateFormatter { static let iso8601MS: Foundation.DateFormatter = { let formatter = Foundation.DateFormatter() formatter.calendar = Calendar(identifier: Calendar.Identifier.iso8601) diff --git a/mambaSharedFramework/HLSInterstitialValueTypes.swift b/mambaSharedFramework/HLSInterstitialValueTypes.swift new file mode 100644 index 0000000..43e1a0d --- /dev/null +++ b/mambaSharedFramework/HLSInterstitialValueTypes.swift @@ -0,0 +1,112 @@ +// +// HLSInterstitialValueTypes.swift +// mamba +// +// Created by Migneco, Ray on 10/22/24. +// + +import Foundation + +/// specifies how the client should align interstitial content to the primary content +public struct HLSInterstitialAlignment: FailableStringLiteralConvertible, Equatable { + + public enum Snap: String, CaseIterable { + /// client SHOULD locate the segment boundary closest to the scheduled resumption point from the + /// interstitial in the Media Playlist of the primary content and resume playback of primary content at that boundary. + case `in` = "IN" + + /// client SHOULD locate the segment boundary closest to the START-DATE of the interstitial in the + /// Media Playlist of the primary content and transition to the interstitial at that boundary. + case out = "OUT" + } + + /// the set of snap options for aligning interstitial content + public let values: Set + + /// creates a snap guide based on provided values + /// + /// - Parameter values: array of `Snap` values + public init(values: [Snap]) { + self.values = Set(values) + } + + /// creates a snap guide based on the provided string value + /// + /// - Parameter string: a comma separated string indicating snap values + public init?(string: String) { + let snapValues = string.components(separatedBy: ",") + .compactMap({ Snap(rawValue: $0 )}) + + guard !snapValues.isEmpty else { return nil } + + self.init(values: snapValues) + } +} + +/// specifies how the player should enforce seek restrictions for the interstitial content +public struct HLSInterstitialSeekRestrictions: FailableStringLiteralConvertible, Equatable { + + public enum Restriction: String, CaseIterable { + /// If the list contains SKIP then while the interstitial is being played, the client MUST NOT + /// allow the user to seek forward from the current playhead position or set the rate to + /// greater than the regular playback rate until playback reaches the end of the interstitial. + case skip = "SKIP" + + /// If the list contains JUMP then the client MUST NOT allow the user to seek from a position + /// in the primary asset earlier than the START-DATE attribute to a position after it without + /// first playing the interstitial asset, even if the interstitial at START-DATE was played + /// through earlier. + case jump = "JUMP" + } + + /// set of restrictions applied to the interstitial content + public let restrictions: Set + + /// Creates a set of restrictions based on provided values + /// + /// - Parameter restrictions: array of `Restriction` + public init(restrictions: [Restriction]) { + self.restrictions = Set(restrictions) + } + + /// creates a snap guide based on the provided string value + /// + /// - Parameter string: a comma separated string indicating snap values + public init?(string: String) { + let restrictions = string.components(separatedBy: ",") + .compactMap({ Restriction(rawValue: $0 )}) + + guard !restrictions.isEmpty else { return nil } + + self.init(restrictions: restrictions) + } +} + +public enum HLSInterstitialTimelineStyle: String, FailableStringLiteralConvertible { + + /// indicates whether the interstitial is intended to be presented as distinct from the content + case highlight = "HIGHLIGHT" + + /// indicates that the interstitial should NOT be presented as differentiated from the content + case primary = "PRIMARY" + + /// Creates a timeline style from the provided string + public init?(string: String) { + self.init(rawValue: string) + } +} + +/// Type that indicates how an interstitial event should be presented on a timeline +public enum HLSInterstitialTimelineOccupation: String, FailableStringLiteralConvertible { + + /// the interstitial should be presented as a single point on the timeline + case point = "POINT" + + /// the interstitial should be presented as a range on the timeline + case range = "RANGE" + + /// Creates a timeline occupation from the provided string + public init?(string: String) { + self.init(rawValue: string) + } +} diff --git a/mambaSharedFramework/HLSValidationIssue.swift b/mambaSharedFramework/HLSValidationIssue.swift index 0ca620c..33599f6 100644 --- a/mambaSharedFramework/HLSValidationIssue.swift +++ b/mambaSharedFramework/HLSValidationIssue.swift @@ -80,5 +80,7 @@ public enum IssueDescription: String { case EXT_X_DATERANGETagPLANNED_DURATIONMustNotBeNegative = "PLANNED-DURATION MUST NOT be negative." case EXT_X_DATERANGEExistsWithNoEXT_X_PROGRAM_DATE_TIME = "If a Playlist contains an EXT-X-DATERANGE tag, it MUST also contain at least one EXT-X-PROGRAM-DATE-TIME tag." case EXT_X_DATERANGEAttributeMismatchForTagsWithSameID = "If a Playlist contains two EXT-X-DATERANGE tags with the same ID attribute value, then any AttributeName that appears in both tags MUST have the same AttributeValue." + case EXT_X_DATERANGEMissingAssetListOrAssetUriAttribute = "A Date Range tag specifying CLASS=com.apple.hls.interstitial must contain either an X-ASSET-LIST OR X-ASSET-URI attribute" + case EXT_X_DATERANGEContainsBothAssetListAndAssetUriAttribute = "A Date Range tag specifying CLASS=com.apple.hls.interstitial cannot contain both an X-ASSET-LIST AND X-ASSET-URI attribute" } diff --git a/mambaSharedFramework/InterstitialTagBuilder.swift b/mambaSharedFramework/InterstitialTagBuilder.swift new file mode 100644 index 0000000..13b3ad3 --- /dev/null +++ b/mambaSharedFramework/InterstitialTagBuilder.swift @@ -0,0 +1,312 @@ +// +// InterstitialTagBuilder.swift +// mamba +// +// Created by Migneco, Ray on 10/22/24. +// Copyright © 2024 Comcast Corporation. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. All rights reserved. +// + +import Foundation + +/// A utility class for configuring and constructing an interstitial tag +/// The properties in this class are in accordance with the HLS spec +/// outlined in `draft-pantos-hls-rfc8216bis-15` Appendix D +/// https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#appendix-D +public final class InterstitialTagBuilder { + + /// An Interstitial EXT-X-DATERANGE tag MUST have a CLASS attribute whose + /// value is "com.apple.hls.interstitial". + static let appleHLSInterstitialClassIdentifier = "com.apple.hls.interstitial" + + /// A quoted-string that uniquely identifies a Date Range in the + /// Playlist. This attribute is REQUIRED + let id: String + + /// required to be "com.apple.hls.interstitial" + let classId: String + + /// date/time at which the Date Range begins. This attribute is REQUIRED. + let startDate: Date + + /// The value of the X-ASSET-URI is a quoted-string absolute URI for a + /// single interstitial asset. An Interstitial EXT-X-DATERANGE tag + /// MUST have either the X-ASSET-URI attribute or the X-ASSET-LIST + /// attribute. It MUST NOT have both. + let assetUri: String? + + /// The value of the X-ASSET-LIST is a quoted-string URI to a JSON + /// object. + let assetList: String? + + /// the duration of the interstitial content in seconds + var duration: Double? + + /// the expected duration of the interstitial content in seconds which can indicate a value when the actual duration is not yet known + var plannedDuration: Double? + + /// The value of X-RESUME-OFFSET is a decimal-floating-point of seconds that specifies where primary playback is to resume + /// following the playback of the interstitial. + var resumeOffset: Double? + + /// The value of X-PLAYOUT-LIMIT is a decimal-floating-point of seconds that specifies a limit for the playout time of the entire interstitial. + var playoutLimit: Double? + + /// The value of the X-SNAP attribute is an enumerated-string-list of Snap Identifiers. + /// The defined Snap Identifiers are: OUT and IN. This attribute is OPTIONAL. + var alignment: HLSInterstitialAlignment? + + /// The value of the X-RESTRICT attribute is an enumerated-string-list of Navigation Restriction Identifiers. The defined Navigation + /// Restriction Identifiers are: SKIP and JUMP. These restrictions are enforced at the player UI level. + var restrictions: HLSInterstitialSeekRestrictions? + + /// This attribute indicates whether the interstitial is intended to be presented as distinct from the content ("HIGHLIGHT") or not differentiated ("PRIMARY"). + var timelineStyle: HLSInterstitialTimelineStyle? + + /// The attribute indicates whether the interstitial should be presented as a single point on the timeline or as a range. + var timelineOccupation: HLSInterstitialTimelineOccupation? + + /// Provides a hint to the client to know how coordinated playback of the same asset will behave across multiple players + var contentMayVary: Bool? + + /// The "X-" prefix defines a namespace reserved for client-defined attributes. The client-attribute MUST be a legal AttributeName. + /// Clients SHOULD use a reverse-DNS syntax when defining their own attribute names to avoid collisions. The attribute value MUST be + /// a quoted-string, a hexadecimal-sequence, or a decimal-floating- point. An example of a client-defined attribute is X-COM-EXAMPLE- + /// AD-ID="XYZ123". These attributes are OPTIONAL. + var clientAttributes: [String: LosslessStringConvertible]? + + /// Creates a Tag Builder using an asset Uri + /// + /// - Parameters: + /// - id: the identifier for the interstitial + /// - startDate: `Date` at which the interstitial begins + /// - assetUri: the URI locating the interstitial + public init(id: String, startDate: Date, assetUri: String) { + self.id = id + self.startDate = startDate + self.assetUri = assetUri + self.assetList = nil + self.classId = Self.appleHLSInterstitialClassIdentifier + } + + /// Creates a Tag Builder using an Asset List Uri + /// + /// - Parameters: + /// - id: the identifier for the interstitial + /// - startDate: `Date` indicating when the interstitial begins + /// - assetList: the URI to a JSON object containing the assets + public init(id: String, startDate: Date, assetList: String) { + self.id = id + self.startDate = startDate + self.assetList = assetList + self.assetUri = nil + self.classId = Self.appleHLSInterstitialClassIdentifier + } + + /// Specifies the duration of the interstitial + /// + /// - Parameter duration: `Double` indicating duration + /// + /// - Returns: an instance of the builder + @discardableResult + public func withDuration(_ duration: Double) -> Self { + self.duration = duration + + return self + } + + /// Specifies the planned duration of the interstitial + /// + /// - Parameter duration: `Double` indicating duration + /// + /// - Returns: an instance of the builder + @discardableResult + public func withPlannedDuration(_ plannedDuration: Double) -> Self { + self.plannedDuration = plannedDuration + + return self + } + + /// Configures the interstitial with a resume offset + /// + /// - Parameter offset: `Double` indicating the resume offset + /// + /// - Returns: an instance of the builder + @discardableResult + public func withResumeOffset(_ offset: Double) -> Self { + self.resumeOffset = offset + + return self + } + + /// Configures the interstitial with a playout limit + /// + /// - Parameter limit: `Double` indicating playout limit + /// + /// - Returns: an instance of the builder + @discardableResult + public func withPlayoutLimit(_ limit: Double) -> Self { + self.playoutLimit = limit + + return self + } + + /// Specifies the alignment of the interstitial with respect to content + /// + /// - Parameter alignment: `HLSInterstitialAlignment` specifying alignment guides + /// + /// - Returns: an instance of the builder + @discardableResult + public func withAlignment(_ alignment: HLSInterstitialAlignment) -> Self { + self.alignment = alignment + + return self + } + + /// Specifies seek restrictions applied to the interstitial + /// + /// - Parameter restrictions: instance of `HLSInterstitialSeekRestrictions` + /// + /// - Returns: an instance of the builder + public func withRestrictions(_ restrictions: HLSInterstitialSeekRestrictions) -> Self { + self.restrictions = restrictions + + return self + } + + /// Specifies how the interstitial is styled on the timeline + /// + /// - Parameter style: `HLSInterstitialTimelineStyle` type + /// + /// - Returns: an instance of the builder + @discardableResult + public func withTimelineStyle(_ style: HLSInterstitialTimelineStyle) -> Self { + self.timelineStyle = style + + return self + } + + /// Describes how the interstitial occupies the content timeline + /// + /// - Parameter occupation: `HLSInterstitialTimelineOccupation` type + /// + /// - Returns: an instance of the builder + @discardableResult + public func withTimelineOccupation(_ occupation: HLSInterstitialTimelineOccupation) -> Self { + self.timelineOccupation = occupation + + return self + } + + /// Indicates if the interstitial content varies or stays the same during a shared watching activity + /// + /// - Parameter variation: `Bool` indicating if there's variation + /// + /// - Returns: an instance of the builder + @discardableResult + public func withContentVariation(_ variation: Bool) -> Self { + self.contentMayVary = variation + + return self + } + + /// Specifies client attributes describing the interstitial + /// + /// - Parameter attributes: a map of `[String: LosslessStringConvertible]` describing the attributes + /// + /// - Returns: an instance of the builder + @discardableResult + public func withClientAttributes(_ attributes: [String: LosslessStringConvertible]) -> Self { + self.clientAttributes = attributes + + return self + } + + /// Builds the DateRange tag utilizing the configured HLS interstitial properties + /// + /// - Returns: `HLSTag` + public func buildTag() -> HLSTag { + + var hlsTagDictionary = HLSTagDictionary() + + hlsTagDictionary[PantosValue.id.rawValue] = HLSValueData(value: id, quoteEscaped: true) + let startDateString = String.DateFormatter.iso8601MS.string(from: startDate) + hlsTagDictionary[PantosValue.startDate.rawValue] = HLSValueData(value: startDateString, + quoteEscaped: true) + hlsTagDictionary[PantosValue.classAttribute.rawValue] = HLSValueData(value: classId, + quoteEscaped: true) + + if let assetUri { + hlsTagDictionary[PantosValue.assetUri.rawValue] = HLSValueData(value: assetUri, quoteEscaped: true) + } + + if let assetList { + hlsTagDictionary[PantosValue.assetList.rawValue] = HLSValueData(value: assetList, quoteEscaped: true) + } + + if let duration { + hlsTagDictionary[PantosValue.duration.rawValue] = HLSValueData(value: String(duration), + quoteEscaped: false) + } + + if let plannedDuration { + hlsTagDictionary[PantosValue.plannedDuration.rawValue] = HLSValueData(value: String(plannedDuration), + quoteEscaped: false) + } + + if let resumeOffset { + hlsTagDictionary[PantosValue.resumeOffset.rawValue] = HLSValueData(value: String(resumeOffset), + quoteEscaped: false) + } + + if let playoutLimit { + hlsTagDictionary[PantosValue.playoutLimit.rawValue] = HLSValueData(value: String(playoutLimit), + quoteEscaped: false) + } + + if let restrictions { + let str = restrictions.restrictions.map({ $0.rawValue }).joined(separator: ",") + hlsTagDictionary[PantosValue.restrict.rawValue] = HLSValueData(value: str, quoteEscaped: true) + } + + if let alignment { + let str = alignment.values.map({ $0.rawValue }).joined(separator: ",") + hlsTagDictionary[PantosValue.snap.rawValue] = HLSValueData(value: str, quoteEscaped: true) + } + + if let timelineStyle { + hlsTagDictionary[PantosValue.timelineStyle.rawValue] = HLSValueData(value: timelineStyle.rawValue, + quoteEscaped: true) + } + + if let timelineOccupation { + hlsTagDictionary[PantosValue.timelineOccupies.rawValue] = HLSValueData(value: timelineOccupation.rawValue, + quoteEscaped: true) + } + + if let contentMayVary { + hlsTagDictionary[PantosValue.contentMayVary.rawValue] = HLSValueData(value: contentMayVary == true ? "YES" : "NO", + quoteEscaped: true) + } + + if let clientAttributes { + for (k, v) in clientAttributes { + hlsTagDictionary[k] = HLSValueData(value: String(v), quoteEscaped: true) + } + } + + return HLSTag(tagDescriptor: PantosTag.EXT_X_DATERANGE, + stringTagData: nil, + parsedValues: hlsTagDictionary) + } +} diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_DATERANGETagValidator.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_DATERANGETagValidator.swift index 555d36d..03e11bd 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_DATERANGETagValidator.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_DATERANGETagValidator.swift @@ -27,6 +27,9 @@ import Foundation /// class EXT_X_DATERANGETagValidator: HLSTagValidator { + /// the required Date Range class identifier for specifying HLS interstitial tags + static let appleHLSInterstitialClassIdentifier = "com.apple.hls.interstitial" + private let genericDictionaryTagValidator: GenericDictionaryTagValidator init() { @@ -44,7 +47,16 @@ class EXT_X_DATERANGETagValidator: HLSTagValidator { HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.scte35Cmd, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.scte35Out, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.scte35In, optional: true, expectedType: String.self), - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.endOnNext, optional: true, expectedType: Bool.self) + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.endOnNext, optional: true, expectedType: Bool.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.assetUri, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.assetList, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.resumeOffset, optional: true, expectedType: Double.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.playoutLimit, optional: true, expectedType: Double.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.snap, optional: true, expectedType: HLSInterstitialAlignment.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.restrict, optional: true, expectedType: HLSInterstitialSeekRestrictions.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.timelineOccupies, optional: true, expectedType: HLSInterstitialTimelineOccupation.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.timelineStyle, optional: true, expectedType: HLSInterstitialTimelineStyle.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.contentMayVary, optional: true, expectedType: Bool.self) ]) } @@ -78,6 +90,10 @@ class EXT_X_DATERANGETagValidator: HLSTagValidator { validationIssues = validationIssues ?? [] validationIssues?.append(contentsOf: negativePlannedDurationIssues) } + if let interstitialIssues = hlsInterstitialValidation(tag: tag) { + validationIssues = validationIssues ?? [] + validationIssues?.append(contentsOf: interstitialIssues) + } return validationIssues } @@ -175,4 +191,26 @@ class EXT_X_DATERANGETagValidator: HLSTagValidator { } return [HLSValidationIssue(description: .EXT_X_DATERANGETagPLANNED_DURATIONMustNotBeNegative, severity: .warning)] } + + /// if a DateRange tag contains `CLASS="com.apple.hls.interstitial"`, it must specify **either** X-ASSET-LIST OR + /// X-ASSET-URI attributes, but **never** both. + private func hlsInterstitialValidation(tag: HLSTag) -> [HLSValidationIssue]? { + guard let classId = tag.value(forValueIdentifier: PantosValue.classAttribute), + classId == Self.appleHLSInterstitialClassIdentifier + else { + return nil + } + + let assetList = tag.value(forValueIdentifier: PantosValue.assetList) + let assetUri = tag.value(forValueIdentifier: PantosValue.assetUri) + + switch(assetUri, assetList) { + case (.some(_), .some(_)): + return [HLSValidationIssue(description: .EXT_X_DATERANGEContainsBothAssetListAndAssetUriAttribute, severity: .warning)] + case (.none, .none): + return [HLSValidationIssue(description: .EXT_X_DATERANGEMissingAssetListOrAssetUriAttribute, severity: .warning)] + default: + return nil + } + } } diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosValue.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosValue.swift index 952d657..b9c2694 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosValue.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosValue.swift @@ -233,13 +233,15 @@ public enum PantosValue: String { /// Found in `.EXT_X_DATERANGE`. /// Used to carry SCTE-35 data. These attributes are OPTIONAL. case scte35Cmd = "SCTE35-CMD" + /// Found in `.EXT_X_DATERANGE`. /// Used to carry SCTE-35 data. These attributes are OPTIONAL. case scte35Out = "SCTE35-OUT" + /// Found in `.EXT_X_DATERANGE`. /// Used to carry SCTE-35 data. These attributes are OPTIONAL. case scte35In = "SCTE35-IN" - + /// Found in `.EXT_X_DATERANGE`. /// An enumerated-string whose value MUST be YES. This attribute /// indicates that the end of the range containing it is equal to the @@ -248,6 +250,118 @@ public enum PantosValue: String { /// after the START-DATE of the range in question. This attribute is /// OPTIONAL. case endOnNext = "END-ON-NEXT" + + /// Found in `.EXT_X_DATERANGE`. + /// + /// The value of the X-ASSET-URI is a quoted-string absolute URI for a + /// single interstitial asset. An Interstitial EXT-X-DATERANGE tag + /// MUST have either the X-ASSET-URI attribute or the X-ASSET-LIST + /// attribute. It MUST NOT have both. + case assetUri = "X-ASSET-URI" + + /// Found in `.EXT_X_DATERANGE`. + /// + /// The value of the X-ASSET-LIST is a quoted-string URI to a JSON + /// object. + case assetList = "X-ASSET-LIST" + + /// Found in `.EXT_X_DATERANGE`. + /// + /// The value of X-RESUME-OFFSET is a decimal-floating-point of + /// seconds that specifies where primary playback is to resume + /// following the playback of the interstitial. It is expressed as a + /// time offset from where the interstitial playback was scheduled on + /// the primary player timeline. A typical value for X-RESUME-OFFSET + /// is zero. This attribute is OPTIONAL. + /// + /// If the X-RESUME-OFFSET is not present, its value is considered to + /// be the duration of the interstitial. This is appropriate for live + /// content, where playback is to be kept at a constant delay from the + /// live edge, or for VOD playback where the HLS interstitial is + /// intended to exactly replace content in the primary asset. + case resumeOffset = "X-RESUME-OFFSET" + + /// Found in `.EXT_X_DATERANGE`. + /// + /// The value of X-PLAYOUT-LIMIT is a decimal-floating-point of + /// seconds that specifies a limit for the playout time of the entire + /// interstitial. If it is present, the client SHOULD end the + /// interstitial if playback reaches that offset from its start. + /// Otherwise the interstitial MUST end upon reaching the end of the + /// interstitial asset(s). This attribute is OPTIONAL. + case playoutLimit = "X-PLAYOUT-LIMIT" + + /// Found in `.EXT_X_DATERANGE`. + /// + /// The value of the X-SNAP attribute is an enumerated-string-list of + /// Snap Identifiers. The defined Snap Identifiers are: OUT and IN. + /// This attribute is OPTIONAL. + /// + /// If the list contains OUT then the client SHOULD locate the segment + /// boundary closest to the START-DATE of the interstitial in the + /// Media Playlist of the primary content and transition to the + /// interstitial at that boundary. If more than one Media Playlist is + /// contributing to playback (audio plus video for example), the + /// client SHOULD transition at the earliest segment boundary. + /// + /// If the list contains IN then the client SHOULD locate the segment + /// boundary closest to the scheduled resumption point from the + /// interstitial in the Media Playlist of the primary content and + /// resume playback of primary content at that boundary. If more than + /// one Media Playlist is contributing to playback, the client SHOULD + /// transition at the latest segment boundary. + case snap = "X-SNAP" + + /// Found in `.EXT_X_DATERANGE`. + /// + /// The value of the X-RESTRICT attribute is an enumerated-string-list + /// of Navigation Restriction Identifiers. The defined Navigation + /// Restriction Identifiers are: SKIP and JUMP. These restrictions + /// are enforced at the player UI level. This attribute is OPTIONAL. + /// + /// If the list contains SKIP then while the interstitial is being + /// played, the client MUST NOT allow the user to seek forward from + /// the current playhead position or set the rate to greater than the + /// regular playback rate until playback reaches the end of the + /// interstitial. + /// + /// If the list contains JUMP then the client MUST NOT allow the user + /// to seek from a position in the primary asset earlier than the + /// START-DATE attribute to a position after it without first playing + /// the interstitial asset, even if the interstitial at START-DATE was + /// played through earlier. If the user attempts to seek across more + /// than one interstitial, the client SHOULD choose at least one + /// interstitial to play before allowing the seek to complete. + case restrict = "X-RESTRICT" + + /// Found in `.EXT_X_DATERANGE`. + /// + /// This attribute may have the value "POINT" or "RANGE". + /// The attribute indicates whether the interstitial should be presented as a + /// single point on the timeline or as a range. + /// If X-TIMELINE-OCCUPIES is missing it is considered to have a value of + /// "POINT" (which is typical for VOD presentations), although clients may infer + /// a value of "RANGE" if the interstitial has positive non-zero resumption offset. + /// This attribute is OPTIONAL + case timelineOccupies = "X-TIMELINE-OCCUPIES" + + /// Found in `.EXT_X_DATERANGE`. + /// + /// This attribute may have the value "HIGHLIGHT" or "PRIMARY". + /// This attribute indicates whether the interstitial is intended to be presented as + /// distinct from the content ("HIGHLIGHT") or not differentiated ("PRIMARY"). + /// If X-TIMELINE-STYLE is missing it is considered to have a value of "HIGHLIGHT" + /// This attribute is OPTIONAL + case timelineStyle = "X-TIMELINE-STYLE" + + /// Found in `.EXT_X_DATERANGE`. + /// + /// Provides a hint to the client to know how coordinated playback of + /// the same asset will behave across multiple players + /// A value of "NO" indicates all players will get the same interstitial content. + /// If this attribute is missing, it is considered to have a value of "YES". + /// This attribute is OPTIONAL + case contentMayVary = "X-CONTENT-MAY-VARY" /// Found in `.EXT_X_SKIP`. /// diff --git a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index 81d3064..a2f5aad 100644 --- a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift +++ b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift @@ -1407,6 +1407,30 @@ class GenericDictionaryTagValidatorTests: XCTestCase { "Expected EXT-X-DATERANGE validation issue (\(expectedValidationIssue.description)) had unexpected severity (\(matchingIssue.severity))") } } + + private func validateInterstitialsEXT_X_DATERANGE(tagData: String, expectedValidationIssues: [HLSValidationIssue]) { + let expectedIssuesDescriptions = expectedValidationIssues.map { $0.description }.joined(separator: "\n") + let (validator, tag) = constructDictionaryValidator(PantosTag.EXT_X_DATERANGE, data: tagData) + guard let errors = validator.validate(tag: tag) else { + if expectedValidationIssues.isEmpty { + return // no issues as expected + } + return XCTFail("Expected EXT-X-DATERANGE validation issue\nTag data: \(tagData)\nExpected issues:\n\(expectedIssuesDescriptions)") + } + let actualIssuesDescriptions = errors.map { $0.description }.joined(separator: "\n") + XCTAssertEqual(errors.count, + expectedValidationIssues.count, + "Mismatch in expected issues and actual issues in EXT_X_DATERANGE validation.\nExpected issues:\n\(expectedIssuesDescriptions)\nActual issues:\n\(actualIssuesDescriptions)") + expectedValidationIssues.forEach { expectedValidationIssue in + guard let matchingIssue = errors.first(where: { $0.description == expectedValidationIssue.description }) else { + return XCTFail("Expected issue \"\(expectedValidationIssue.description)\" not found for EXT-X-DATERANGE tag: \(tagData)\nIssues found:\n\(actualIssuesDescriptions)") + } + XCTAssertEqual(expectedValidationIssue.description, matchingIssue.description) + XCTAssertEqual(expectedValidationIssue.severity, + matchingIssue.severity, + "Expected EXT-X-DATERANGE validation issue (\(expectedValidationIssue.description)) had unexpected severity (\(matchingIssue.severity))") + } + } /* A server produces a Playlist Delta Update (Section 6.2.5.1), by @@ -1449,4 +1473,58 @@ class GenericDictionaryTagValidatorTests: XCTestCase { mandatory: mandatory, badValues: badValues) } + + // MARK: - tests for validating DateRange tags with interstitial attributes + func testParseInterstitialWithAssetUriSuccessful() { + let assetUriString = + """ + ID="ad1",CLASS="com.apple.hls.interstitial",START-DATE="2020-01-02T21:55:44.000Z",DURATION=15.0,X-ASSET-URI="http://example.com/ad1.m3u8",X-RESUME-OFFSET=0,X-RESTRICT="SKIP,JUMP",X-COM-EXAMPLE-BEACON=123 + """ + + let (validator, tag) = constructDictionaryValidator(PantosTag.EXT_X_DATERANGE, data: assetUriString) + XCTAssertNil(validator.validate(tag: tag)) + } + + func testParseInterstitialWithAssetListSuccessful() { + let assetListString = + """ + ID="ad1",CLASS="com.apple.hls.interstitial",START-DATE="2020-01-02T21:55:44.000Z",DURATION=15.0,X-ASSET-LIST="http://example.com/adList",X-RESUME-OFFSET=0,X-RESTRICT="SKIP,JUMP",X-COM-EXAMPLE-BEACON=123 + """ + + let (validator, tag) = constructDictionaryValidator(PantosTag.EXT_X_DATERANGE, data: assetListString) + XCTAssertNil(validator.validate(tag: tag)) + } + + func testInterstitialMissingAssetListOrUriValidationIssue() { + let missingAssetListOrUri = + """ + ID="ad1",CLASS="com.apple.hls.interstitial",START-DATE="2020-01-02T21:55:44.000Z",DURATION=15.0,X-RESUME-OFFSET=0,X-RESTRICT="SKIP,JUMP",X-COM-EXAMPLE-BEACON=123 + """ + + let (validator, tag) = constructDictionaryValidator(PantosTag.EXT_X_DATERANGE, data: missingAssetListOrUri) + + guard let issues = validator.validate(tag: tag), !issues.isEmpty else { + XCTFail("Expected validation issues") + return + } + + XCTAssertEqual(issues.first?.description, IssueDescription.EXT_X_DATERANGEMissingAssetListOrAssetUriAttribute.rawValue) + } + + func testInterstitialContainsBothAssetListAndAssetUriAttributes() { + let assetContainsBothUriAndList = + """ + ID="ad1",CLASS="com.apple.hls.interstitial",START-DATE="2020-01-02T21:55:44.000Z",DURATION=15.0,X-ASSET-LIST="http://example.com/adList",X-ASSET-URI="http://example.com/ad1.m3u8",X-RESUME-OFFSET=0,X-RESTRICT="SKIP,JUMP",X-COM-EXAMPLE-BEACON=123 + """ + + let (validator, tag) = constructDictionaryValidator(PantosTag.EXT_X_DATERANGE, data: assetContainsBothUriAndList) + + guard let issues = validator.validate(tag: tag), !issues.isEmpty else { + XCTFail("Expected validation issues") + return + } + + XCTAssertEqual(issues.first?.description, IssueDescription.EXT_X_DATERANGEContainsBothAssetListAndAssetUriAttribute.rawValue) + } } + diff --git a/mambaTests/Util Tests/InterstitialTagBuilderTests.swift b/mambaTests/Util Tests/InterstitialTagBuilderTests.swift new file mode 100644 index 0000000..ad41b5a --- /dev/null +++ b/mambaTests/Util Tests/InterstitialTagBuilderTests.swift @@ -0,0 +1,113 @@ +// +// InterstitialTagBuilderTests.swift +// mambaTests +// +// Created by Migneco, Ray on 10/23/24. +// Copyright © 2024 Comcast Corporation. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. All rights reserved. +// + +import XCTest + +@testable import mamba + + +final class InterstitialTagBuilderTests: XCTestCase { + + func testTagBuilder() { + + let startDate = Date() + let id: String = "12345" + let assetUri: String = "http://not.a.real.uri" + let assetListUri: String = "http://not.a.real.list" + + let validator = EXT_X_DATERANGETagValidator() + + // test URI + var tagBuilder = InterstitialTagBuilder(id: id, + startDate: startDate, + assetUri: assetUri) + + var tag = decorateAndTest(tagBuilder) + + XCTAssertEqual(tag.value(forValueIdentifier: PantosValue.startDate), String.DateFormatter.iso8601MS.string(from: startDate)) + XCTAssertEqual(tag.value(forValueIdentifier: PantosValue.id), id) + XCTAssertEqual(tag.value(forValueIdentifier: PantosValue.assetUri), assetUri) + XCTAssertNil(tag.value(forValueIdentifier: PantosValue.assetList)) + + XCTAssertNil(validator.validate(tag: tag)) + + // test asset list + tagBuilder = InterstitialTagBuilder(id: id, + startDate: startDate, + assetList: assetListUri) + + tag = decorateAndTest(tagBuilder) + + XCTAssertEqual(tag.value(forValueIdentifier: PantosValue.assetList), assetListUri) + XCTAssertNil(tag.value(forValueIdentifier: PantosValue.assetUri)) + + XCTAssertNil(validator.validate(tag: tag)) + } + + func decorateAndTest(_ tagBuilder: InterstitialTagBuilder) -> HLSTag { + + let duration: Double = 10.0 + let plannedDuration: Double = 10.0 + let alignment = HLSInterstitialAlignment(values: [.in, .out]) + let restrictions = HLSInterstitialSeekRestrictions(restrictions: [.skip, .jump]) + let playoutLimit: Double = 30.0 + let resumeOffset: Double = 5.0 + let timelineStyle = HLSInterstitialTimelineStyle.highlight + let timelineOccupation = HLSInterstitialTimelineOccupation.point + let contentVariation = false + let clientAttributes: [String: LosslessStringConvertible] = ["X-COM-BEACON-URI": "http://not.a.real.beacon", + "X-COM-AD-PROVIDER-ID": 100] + + let tag = tagBuilder + .withDuration(duration) + .withPlannedDuration(plannedDuration) + .withAlignment(alignment) + .withRestrictions(restrictions) + .withPlayoutLimit(playoutLimit) + .withResumeOffset(resumeOffset) + .withTimelineStyle(timelineStyle) + .withTimelineOccupation(timelineOccupation) + .withContentVariation(contentVariation) + .withClientAttributes(clientAttributes) + .buildTag() + + XCTAssertEqual(tag.value(forValueIdentifier: PantosValue.duration), duration) + XCTAssertEqual(tag.value(forValueIdentifier: PantosValue.plannedDuration), plannedDuration) + XCTAssertEqual(tag.value(forValueIdentifier: PantosValue.snap), alignment) + XCTAssertEqual(tag.value(forValueIdentifier: PantosValue.restrict), restrictions) + XCTAssertEqual(tag.value(forValueIdentifier: PantosValue.playoutLimit), playoutLimit) + XCTAssertEqual(tag.value(forValueIdentifier: PantosValue.resumeOffset), resumeOffset) + XCTAssertEqual(tag.value(forValueIdentifier: PantosValue.timelineStyle), timelineStyle) + XCTAssertEqual(tag.value(forValueIdentifier: PantosValue.timelineOccupies), timelineOccupation) + XCTAssertEqual(tag.value(forValueIdentifier: PantosValue.contentMayVary), contentVariation) + + // check client attributes + for (k, v) in clientAttributes { + guard let val = tag.value(forKey: k) else { + XCTFail("Expected to find value for key \(k)") + continue + } + + XCTAssertEqual(val, v.description) + } + + return tag + } + +} diff --git a/mambaTests/Util Tests/Value Types/HLSInterstitialValueTests.swift b/mambaTests/Util Tests/Value Types/HLSInterstitialValueTests.swift new file mode 100644 index 0000000..7f83c7f --- /dev/null +++ b/mambaTests/Util Tests/Value Types/HLSInterstitialValueTests.swift @@ -0,0 +1,59 @@ +// +// HLSInterstitialValueTests.swift +// mambaTests +// +// Created by Migneco, Ray on 10/22/24. +// Copyright © 2024 Comcast Corporation. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. All rights reserved. +// + +import XCTest + +@testable import mamba + +final class HLSInterstitialValueTests: XCTestCase { + + func testSnapAlignment() { + var vals = HLSInterstitialAlignment.Snap.allCases + + XCTAssertEqual(HLSInterstitialAlignment(values: vals).values.count, 2) + + // test de-duping + vals.append(HLSInterstitialAlignment.Snap.in) + XCTAssertEqual(HLSInterstitialAlignment(values: vals).values.count, 2) + + // create from string + let inputStr = "IN,OUT" + XCTAssertEqual(HLSInterstitialAlignment(string: inputStr)?.values.count, 2) + + let badInput = "up,down" + XCTAssertNil(HLSInterstitialAlignment(string: badInput)) + } + + func testRestrictions() { + var vals = HLSInterstitialSeekRestrictions.Restriction.allCases + + XCTAssertEqual(HLSInterstitialSeekRestrictions(restrictions: vals).restrictions.count, 2) + + // de-dupe + vals.append(HLSInterstitialSeekRestrictions.Restriction.jump) + XCTAssertEqual(HLSInterstitialSeekRestrictions(restrictions: vals).restrictions.count, 2) + + let inputStr = "SKIP,JUMP" + XCTAssertEqual(HLSInterstitialSeekRestrictions(string: inputStr)?.restrictions.count, 2) + + let badInput = "Forward,Back" + XCTAssertNil(HLSInterstitialSeekRestrictions(string: badInput)) + } + +}