From 7dbeace10ec8a3e7364ccebb243728d7c56f2188 Mon Sep 17 00:00:00 2001 From: Ray Migneco Date: Tue, 22 Oct 2024 12:22:42 -0400 Subject: [PATCH 01/13] Added PantosValue for Interstitial Tags --- .../PantosValue.swift | 116 +++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) 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`. /// From a2c61ef96128e5b511fe74097778c69630454ca8 Mon Sep 17 00:00:00 2001 From: Ray Migneco Date: Tue, 22 Oct 2024 14:02:49 -0400 Subject: [PATCH 02/13] Added Validation Issues for HLS Interstitial tags - checking that with HLS CLASS attribute, only asset list or id can be accepted --- mambaSharedFramework/HLSValidationIssue.swift | 2 + .../EXT_X_DATERANGETagValidator.swift | 41 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) 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/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..b0ba5dc 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,17 @@ 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) + +// case snap = "X-SNAP" +// case restrict = "X-RESTRICT" +// case timelineOccupies = "X-TIMELINE-OCCUPIES" +// case timelineStyle = "X-TIMELINE-STYLE" +// case contentMayVary = "X-CONTENT-MAY-VARY" ]) } @@ -78,6 +91,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 +192,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 + } + } } From d6ea03b251c6cb8c56ae14935fe2ccf952e9b5f2 Mon Sep 17 00:00:00 2001 From: Ray Migneco Date: Tue, 22 Oct 2024 15:15:21 -0400 Subject: [PATCH 03/13] Added Special Value types for interstital attributes - completed the Generic Tag validator for Date Range --- .../HLSInterstitialValueTypes.swift | 112 ++++++++++++++++++ .../EXT_X_DATERANGETagValidator.swift | 17 ++- 2 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 mambaSharedFramework/HLSInterstitialValueTypes.swift diff --git a/mambaSharedFramework/HLSInterstitialValueTypes.swift b/mambaSharedFramework/HLSInterstitialValueTypes.swift new file mode 100644 index 0000000..c868e09 --- /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 HLSInterstitialSnapGuide: FailableStringLiteralConvertible { + + public enum Snap: String { + /// 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 snapIn = "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 snapOut = "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 { + + public enum Restriction: String { + /// 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/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 b0ba5dc..4f79474 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 @@ -51,13 +51,12 @@ class EXT_X_DATERANGETagValidator: HLSTagValidator { 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) - -// case snap = "X-SNAP" -// case restrict = "X-RESTRICT" -// case timelineOccupies = "X-TIMELINE-OCCUPIES" -// case timelineStyle = "X-TIMELINE-STYLE" -// case contentMayVary = "X-CONTENT-MAY-VARY" + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.playoutLimit, optional: true, expectedType: Double.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.snap, optional: true, expectedType: HLSInterstitialSnapGuide.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) ]) } @@ -193,8 +192,8 @@ 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. + /// 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 From 63e374316756cd0ab42818fac3f38532fcdfd3dd Mon Sep 17 00:00:00 2001 From: Ray Migneco Date: Tue, 22 Oct 2024 16:09:56 -0400 Subject: [PATCH 04/13] Added additional date range validation tests --- .../GenericDictionaryTagValidatorTests.swift | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) 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) + } } + From 60ef9c69c020eb862dacf63d729d9f5620639643 Mon Sep 17 00:00:00 2001 From: Ray Migneco Date: Tue, 22 Oct 2024 16:26:51 -0400 Subject: [PATCH 05/13] Added Interstitial Value Tests - testing seek restrictions and alignment - renamed Interstitial Alignment --- mamba.xcodeproj/project.pbxproj | 96 +++++++++++-------- .../HLSInterstitialValueTypes.swift | 6 +- .../EXT_X_DATERANGETagValidator.swift | 2 +- .../HLSInterstitialValueTests.swift | 59 ++++++++++++ 4 files changed, 119 insertions(+), 44 deletions(-) create mode 100644 mambaTests/Util Tests/Value Types/HLSInterstitialValueTests.swift diff --git a/mamba.xcodeproj/project.pbxproj b/mamba.xcodeproj/project.pbxproj index 31f1514..616d729 100644 --- a/mamba.xcodeproj/project.pbxproj +++ b/mamba.xcodeproj/project.pbxproj @@ -61,6 +61,24 @@ 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 */; }; 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 +204,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 +359,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 +472,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 +662,12 @@ 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 = ""; }; 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 +781,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 +837,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 +981,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,6 +1126,7 @@ EC7491AF1DD29D5C00AF4E20 /* HLSValidationIssue.swift */, EC7491B11DD29D5C00AF4E20 /* HLSValueTypes.swift */, EC7491B21DD29D5C00AF4E20 /* HLSWriter.swift */, + E6B2D8052CC834F500D319DF /* HLSInterstitialValueTypes.swift */, EC9547871E5CC84700962535 /* FrameworkInfo.swift */, EC1521511DD28536006FB265 /* mamba.h */, ); @@ -1279,17 +1299,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 = ( @@ -1342,6 +1351,7 @@ EC7492B11DD29F8900AF4E20 /* HLSPlaylistTypeTests.swift */, EC7492B21DD29F8900AF4E20 /* HLSResolutionTests.swift */, 1447583C2C8693E000D12CCD /* HLSVideoLayoutTests.swift */, + E6B2D8092CC83E5B00D319DF /* HLSInterstitialValueTests.swift */, ); path = "Value Types"; sourceTree = ""; @@ -1349,7 +1359,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 +1805,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 +1835,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 */, @@ -1871,11 +1884,9 @@ F73183771E78758B00ED8E59 /* HLSStringRefFactory.m 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 */, @@ -1939,6 +1950,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 +1977,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 +2007,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 */, @@ -2041,11 +2056,9 @@ F73183781E78758B00ED8E59 /* HLSStringRefFactory.m 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 */, @@ -2109,6 +2122,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 +2149,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 +2175,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 +2224,9 @@ 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 */, 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 */, @@ -2279,6 +2294,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/HLSInterstitialValueTypes.swift b/mambaSharedFramework/HLSInterstitialValueTypes.swift index c868e09..7b938a4 100644 --- a/mambaSharedFramework/HLSInterstitialValueTypes.swift +++ b/mambaSharedFramework/HLSInterstitialValueTypes.swift @@ -8,9 +8,9 @@ import Foundation /// specifies how the client should align interstitial content to the primary content -public struct HLSInterstitialSnapGuide: FailableStringLiteralConvertible { +public struct HLSInterstitialAlignment: FailableStringLiteralConvertible { - public enum Snap: String { + 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 snapIn = "IN" @@ -46,7 +46,7 @@ public struct HLSInterstitialSnapGuide: FailableStringLiteralConvertible { /// specifies how the player should enforce seek restrictions for the interstitial content public struct HLSInterstitialSeekRestrictions: FailableStringLiteralConvertible { - public enum Restriction: String { + 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. 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 4f79474..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 @@ -52,7 +52,7 @@ class EXT_X_DATERANGETagValidator: HLSTagValidator { 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: HLSInterstitialSnapGuide.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), diff --git a/mambaTests/Util Tests/Value Types/HLSInterstitialValueTests.swift b/mambaTests/Util Tests/Value Types/HLSInterstitialValueTests.swift new file mode 100644 index 0000000..75fd44d --- /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.snapIn) + 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)) + } + +} From a94bf9789ba2dd460cf0ca01de331e3243a709d4 Mon Sep 17 00:00:00 2001 From: Ray Migneco Date: Tue, 22 Oct 2024 18:22:39 -0400 Subject: [PATCH 06/13] Added interstitial tag builder - included required initializers - included builder functions --- mamba.xcodeproj/project.pbxproj | 8 + .../InterstitialTagBuilder.swift | 200 ++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 mambaSharedFramework/InterstitialTagBuilder.swift diff --git a/mamba.xcodeproj/project.pbxproj b/mamba.xcodeproj/project.pbxproj index 616d729..d20b27f 100644 --- a/mamba.xcodeproj/project.pbxproj +++ b/mamba.xcodeproj/project.pbxproj @@ -79,6 +79,9 @@ 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 */; }; 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 */; }; @@ -668,6 +671,7 @@ 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 = ""; }; 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 = ""; }; @@ -1129,6 +1133,7 @@ E6B2D8052CC834F500D319DF /* HLSInterstitialValueTypes.swift */, EC9547871E5CC84700962535 /* FrameworkInfo.swift */, EC1521511DD28536006FB265 /* mamba.h */, + E6B2D80D2CC842DB00D319DF /* InterstitialTagBuilder.swift */, ); path = mambaSharedFramework; sourceTree = ""; @@ -1882,6 +1887,7 @@ 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 */, EC03B6681E5CC56B00BF1F97 /* RapidParserLineState.c in Sources */, @@ -2054,6 +2060,7 @@ 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 */, EC03B6691E5CC56B00BF1F97 /* RapidParserLineState.c in Sources */, @@ -2226,6 +2233,7 @@ EC1CCD02209A2CF9006B59FF /* HLSStringRef_ConcreteNSString.m in Sources */, EC1CCD51209A2CF9006B59FF /* HLSPlaylistValidator.swift in Sources */, EC1CCD5F209A2CF9006B59FF /* HLSTagWriter.swift in Sources */, + E6B2D80F2CC842DB00D319DF /* InterstitialTagBuilder.swift in Sources */, EC1CCD49209A2CF9006B59FF /* HLSPlaylistCollectionValidator.swift in Sources */, EC1CCD28209A2CF9006B59FF /* FailableStringLiteralConvertible.swift in Sources */, EC1CCCF7209A2CF9006B59FF /* StructureState.swift in Sources */, diff --git a/mambaSharedFramework/InterstitialTagBuilder.swift b/mambaSharedFramework/InterstitialTagBuilder.swift new file mode 100644 index 0000000..674a452 --- /dev/null +++ b/mambaSharedFramework/InterstitialTagBuilder.swift @@ -0,0 +1,200 @@ +// +// 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 +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 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 + } + + /// Configures the interstitial with a resume offset + /// + /// - Parameter offset: `Double` indicating the resume offset + /// + /// - Returns: an instance of the builder + 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 + 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 + 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 + 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 + 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 + 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 + public func withClientAttributes(_ attributes: [String: LosslessStringConvertible]) -> Self { + self.clientAttributes = attributes + + return self + } + + public func buildTag() { + + } +} From 76a6b3205167ab2add009098dd6ff9a6a36e6986 Mon Sep 17 00:00:00 2001 From: Ray Migneco Date: Wed, 23 Oct 2024 09:51:34 -0400 Subject: [PATCH 07/13] Changed visibility of Dateformatter so they could be re-use --- .../HLS Utils/String Util/String+DateParsing.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 9b66030fa3ee6b575875c9b321d58d4f03edb607 Mon Sep 17 00:00:00 2001 From: Ray Migneco Date: Wed, 23 Oct 2024 09:52:13 -0400 Subject: [PATCH 08/13] Added discardable result to builder functions - completed tag return in build func - added documentation --- .../InterstitialTagBuilder.swift | 95 ++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/mambaSharedFramework/InterstitialTagBuilder.swift b/mambaSharedFramework/InterstitialTagBuilder.swift index 674a452..a17ea3c 100644 --- a/mambaSharedFramework/InterstitialTagBuilder.swift +++ b/mambaSharedFramework/InterstitialTagBuilder.swift @@ -22,6 +22,9 @@ 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 @@ -48,6 +51,9 @@ public final class InterstitialTagBuilder { /// object. let assetList: String? + /// the duration of the interstitial content in seconds + var duration: 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? @@ -106,11 +112,24 @@ public final class InterstitialTagBuilder { 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 + } + /// 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 @@ -122,6 +141,7 @@ public final class InterstitialTagBuilder { /// - Parameter limit: `Double` indicating playout limit /// /// - Returns: an instance of the builder + @discardableResult public func withPlayoutLimit(_ limit: Double) -> Self { self.playoutLimit = limit @@ -133,6 +153,7 @@ public final class InterstitialTagBuilder { /// - Parameter alignment: `HLSInterstitialAlignment` specifying alignment guides /// /// - Returns: an instance of the builder + @discardableResult public func withAlignment(_ alignment: HLSInterstitialAlignment) -> Self { self.alignment = alignment @@ -155,6 +176,7 @@ public final class InterstitialTagBuilder { /// - Parameter style: `HLSInterstitialTimelineStyle` type /// /// - Returns: an instance of the builder + @discardableResult public func withTimelineStyle(_ style: HLSInterstitialTimelineStyle) -> Self { self.timelineStyle = style @@ -166,6 +188,7 @@ public final class InterstitialTagBuilder { /// - Parameter occupation: `HLSInterstitialTimelineOccupation` type /// /// - Returns: an instance of the builder + @discardableResult public func withTimelineOccupation(_ occupation: HLSInterstitialTimelineOccupation) -> Self { self.timelineOccupation = occupation @@ -177,6 +200,7 @@ public final class InterstitialTagBuilder { /// - Parameter variation: `Bool` indicating if there's variation /// /// - Returns: an instance of the builder + @discardableResult public func withContentVariation(_ variation: Bool) -> Self { self.contentMayVary = variation @@ -188,13 +212,82 @@ public final class InterstitialTagBuilder { /// - 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 } - public func buildTag() { + /// Builds the DateRange tag utilizing the configured HLS interstitial properties + /// + /// - Returns: `HLSTag` + public func buildTag() -> HLSTag { + + var hlsTagDictionary = HLSTagDictionary() + + let startDateString = String.DateFormatter.iso8601MS.string(from: startDate) + hlsTagDictionary[PantosValue.startDate.rawValue] = HLSValueData(value: startDateString, + quoteEscaped: true) + hlsTagDictionary[PantosValue.id.rawValue] = HLSValueData(value: id, 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: true) + } + + if let resumeOffset { + hlsTagDictionary[PantosValue.resumeOffset.rawValue] = HLSValueData(value: String(resumeOffset), + quoteEscaped: true) + } + + if let playoutLimit { + hlsTagDictionary[PantosValue.playoutLimit.rawValue] = HLSValueData(value: String(playoutLimit), + quoteEscaped: true) + } + + 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) } } From 47617fa15abed024282bab69a992877a3600314a Mon Sep 17 00:00:00 2001 From: Ray Migneco Date: Wed, 23 Oct 2024 11:18:49 -0400 Subject: [PATCH 09/13] Added Tag Builder Tests - ensure HLS Interstitial Value types conform to equatable --- mamba.xcodeproj/project.pbxproj | 8 ++ .../HLSInterstitialValueTypes.swift | 4 +- .../InterstitialTagBuilder.swift | 2 +- .../InterstitialTagBuilderTests.swift | 112 ++++++++++++++++++ 4 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 mambaTests/Util Tests/InterstitialTagBuilderTests.swift diff --git a/mamba.xcodeproj/project.pbxproj b/mamba.xcodeproj/project.pbxproj index d20b27f..9758c5c 100644 --- a/mamba.xcodeproj/project.pbxproj +++ b/mamba.xcodeproj/project.pbxproj @@ -82,6 +82,9 @@ 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 */; }; @@ -672,6 +675,7 @@ 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 = ""; }; @@ -1343,6 +1347,7 @@ EC073F5F1FE08F7500689228 /* String+Helio.swift */, EC7492A61DD29F7000AF4E20 /* URL+hlsplaylistTests.swift */, EC9BCAA21D749D8B0032BEBE /* Value Types */, + E6B2D8112CC937D400D319DF /* InterstitialTagBuilderTests.swift */, ); path = "Util Tests"; sourceTree = ""; @@ -1914,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 */, @@ -2087,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 */, @@ -2260,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 */, diff --git a/mambaSharedFramework/HLSInterstitialValueTypes.swift b/mambaSharedFramework/HLSInterstitialValueTypes.swift index 7b938a4..f689695 100644 --- a/mambaSharedFramework/HLSInterstitialValueTypes.swift +++ b/mambaSharedFramework/HLSInterstitialValueTypes.swift @@ -8,7 +8,7 @@ import Foundation /// specifies how the client should align interstitial content to the primary content -public struct HLSInterstitialAlignment: FailableStringLiteralConvertible { +public struct HLSInterstitialAlignment: FailableStringLiteralConvertible, Equatable { public enum Snap: String, CaseIterable { /// client SHOULD locate the segment boundary closest to the scheduled resumption point from the @@ -44,7 +44,7 @@ public struct HLSInterstitialAlignment: FailableStringLiteralConvertible { } /// specifies how the player should enforce seek restrictions for the interstitial content -public struct HLSInterstitialSeekRestrictions: FailableStringLiteralConvertible { +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 diff --git a/mambaSharedFramework/InterstitialTagBuilder.swift b/mambaSharedFramework/InterstitialTagBuilder.swift index a17ea3c..3a1303d 100644 --- a/mambaSharedFramework/InterstitialTagBuilder.swift +++ b/mambaSharedFramework/InterstitialTagBuilder.swift @@ -226,10 +226,10 @@ public final class InterstitialTagBuilder { 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.id.rawValue] = HLSValueData(value: id, quoteEscaped: true) hlsTagDictionary[PantosValue.classAttribute.rawValue] = HLSValueData(value: classId, quoteEscaped: true) diff --git a/mambaTests/Util Tests/InterstitialTagBuilderTests.swift b/mambaTests/Util Tests/InterstitialTagBuilderTests.swift new file mode 100644 index 0000000..451d3ff --- /dev/null +++ b/mambaTests/Util Tests/InterstitialTagBuilderTests.swift @@ -0,0 +1,112 @@ +// +// 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 alignment = HLSInterstitialAlignment(values: [.snapIn, .snapOut]) + 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] + + // TOOD: client attributes + + let tag = tagBuilder + .withDuration(duration) + .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.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 + } + +} From 1b55efd07c8fc037ac85a6ee42ab6cccfa91a16a Mon Sep 17 00:00:00 2001 From: Ray Migneco Date: Wed, 23 Oct 2024 11:31:58 -0400 Subject: [PATCH 10/13] Removed TODO comment --- mambaTests/Util Tests/InterstitialTagBuilderTests.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/mambaTests/Util Tests/InterstitialTagBuilderTests.swift b/mambaTests/Util Tests/InterstitialTagBuilderTests.swift index 451d3ff..00d65b8 100644 --- a/mambaTests/Util Tests/InterstitialTagBuilderTests.swift +++ b/mambaTests/Util Tests/InterstitialTagBuilderTests.swift @@ -73,8 +73,6 @@ final class InterstitialTagBuilderTests: XCTestCase { let clientAttributes: [String: LosslessStringConvertible] = ["X-COM-BEACON-URI": "http://not.a.real.beacon", "X-COM-AD-PROVIDER-ID": 100] - // TOOD: client attributes - let tag = tagBuilder .withDuration(duration) .withAlignment(alignment) From 89b1726a674767a6d98691bd74f82b5d826d9b77 Mon Sep 17 00:00:00 2001 From: Ray Migneco Date: Mon, 28 Oct 2024 10:26:45 -0400 Subject: [PATCH 11/13] Updated Snap enum case names - updated affected tests --- mambaSharedFramework/HLSInterstitialValueTypes.swift | 4 ++-- mambaTests/Util Tests/InterstitialTagBuilderTests.swift | 2 +- .../Util Tests/Value Types/HLSInterstitialValueTests.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mambaSharedFramework/HLSInterstitialValueTypes.swift b/mambaSharedFramework/HLSInterstitialValueTypes.swift index f689695..43e1a0d 100644 --- a/mambaSharedFramework/HLSInterstitialValueTypes.swift +++ b/mambaSharedFramework/HLSInterstitialValueTypes.swift @@ -13,11 +13,11 @@ public struct HLSInterstitialAlignment: FailableStringLiteralConvertible, Equata 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 snapIn = "IN" + 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 snapOut = "OUT" + case out = "OUT" } /// the set of snap options for aligning interstitial content diff --git a/mambaTests/Util Tests/InterstitialTagBuilderTests.swift b/mambaTests/Util Tests/InterstitialTagBuilderTests.swift index 00d65b8..95ff0b4 100644 --- a/mambaTests/Util Tests/InterstitialTagBuilderTests.swift +++ b/mambaTests/Util Tests/InterstitialTagBuilderTests.swift @@ -63,7 +63,7 @@ final class InterstitialTagBuilderTests: XCTestCase { func decorateAndTest(_ tagBuilder: InterstitialTagBuilder) -> HLSTag { let duration: Double = 10.0 - let alignment = HLSInterstitialAlignment(values: [.snapIn, .snapOut]) + let alignment = HLSInterstitialAlignment(values: [.in, .out]) let restrictions = HLSInterstitialSeekRestrictions(restrictions: [.skip, .jump]) let playoutLimit: Double = 30.0 let resumeOffset: Double = 5.0 diff --git a/mambaTests/Util Tests/Value Types/HLSInterstitialValueTests.swift b/mambaTests/Util Tests/Value Types/HLSInterstitialValueTests.swift index 75fd44d..7f83c7f 100644 --- a/mambaTests/Util Tests/Value Types/HLSInterstitialValueTests.swift +++ b/mambaTests/Util Tests/Value Types/HLSInterstitialValueTests.swift @@ -29,7 +29,7 @@ final class HLSInterstitialValueTests: XCTestCase { XCTAssertEqual(HLSInterstitialAlignment(values: vals).values.count, 2) // test de-duping - vals.append(HLSInterstitialAlignment.Snap.snapIn) + vals.append(HLSInterstitialAlignment.Snap.in) XCTAssertEqual(HLSInterstitialAlignment(values: vals).values.count, 2) // create from string From c7f6484d1b63ca4669532a01c23d4dda184047db Mon Sep 17 00:00:00 2001 From: Ray Migneco Date: Mon, 28 Oct 2024 10:28:52 -0400 Subject: [PATCH 12/13] Trim whitespace --- mambaSharedFramework/InterstitialTagBuilder.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/mambaSharedFramework/InterstitialTagBuilder.swift b/mambaSharedFramework/InterstitialTagBuilder.swift index 3a1303d..0931148 100644 --- a/mambaSharedFramework/InterstitialTagBuilder.swift +++ b/mambaSharedFramework/InterstitialTagBuilder.swift @@ -19,8 +19,6 @@ 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 From c3fff36b53543741d6977bf0fa9905ed16192c50 Mon Sep 17 00:00:00 2001 From: Ray Migneco Date: Mon, 28 Oct 2024 10:55:23 -0400 Subject: [PATCH 13/13] Added `plannedDuration` to interstitial tag builder - fixed qutoe escape issues for numerical values in builder - upated unit tests --- .../InterstitialTagBuilder.swift | 27 ++++++++++++++++--- .../InterstitialTagBuilderTests.swift | 3 +++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/mambaSharedFramework/InterstitialTagBuilder.swift b/mambaSharedFramework/InterstitialTagBuilder.swift index 0931148..13b3ad3 100644 --- a/mambaSharedFramework/InterstitialTagBuilder.swift +++ b/mambaSharedFramework/InterstitialTagBuilder.swift @@ -52,6 +52,9 @@ public final class InterstitialTagBuilder { /// 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? @@ -122,6 +125,18 @@ public final class InterstitialTagBuilder { 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 @@ -240,17 +255,23 @@ public final class InterstitialTagBuilder { } if let duration { - hlsTagDictionary[PantosValue.duration.rawValue] = HLSValueData(value: String(duration), quoteEscaped: true) + 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: true) + quoteEscaped: false) } if let playoutLimit { hlsTagDictionary[PantosValue.playoutLimit.rawValue] = HLSValueData(value: String(playoutLimit), - quoteEscaped: true) + quoteEscaped: false) } if let restrictions { diff --git a/mambaTests/Util Tests/InterstitialTagBuilderTests.swift b/mambaTests/Util Tests/InterstitialTagBuilderTests.swift index 95ff0b4..ad41b5a 100644 --- a/mambaTests/Util Tests/InterstitialTagBuilderTests.swift +++ b/mambaTests/Util Tests/InterstitialTagBuilderTests.swift @@ -63,6 +63,7 @@ final class InterstitialTagBuilderTests: XCTestCase { 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 @@ -75,6 +76,7 @@ final class InterstitialTagBuilderTests: XCTestCase { let tag = tagBuilder .withDuration(duration) + .withPlannedDuration(plannedDuration) .withAlignment(alignment) .withRestrictions(restrictions) .withPlayoutLimit(playoutLimit) @@ -86,6 +88,7 @@ final class InterstitialTagBuilderTests: XCTestCase { .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)