diff --git a/mamba.xcodeproj/project.pbxproj b/mamba.xcodeproj/project.pbxproj index 5e0b336..ba2d471 100644 --- a/mamba.xcodeproj/project.pbxproj +++ b/mamba.xcodeproj/project.pbxproj @@ -64,6 +64,9 @@ E65FB2462CD5241D00BF6F56 /* InterstitialValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65FB2452CD5241D00BF6F56 /* InterstitialValueTests.swift */; }; E65FB2472CD5241D00BF6F56 /* InterstitialValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65FB2452CD5241D00BF6F56 /* InterstitialValueTests.swift */; }; E65FB2482CD5241D00BF6F56 /* InterstitialValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65FB2452CD5241D00BF6F56 /* InterstitialValueTests.swift */; }; + E65FB24A2CD524BF00BF6F56 /* InterstitialTagBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65FB2492CD524BF00BF6F56 /* InterstitialTagBuilder.swift */; }; + E65FB24B2CD524BF00BF6F56 /* InterstitialTagBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65FB2492CD524BF00BF6F56 /* InterstitialTagBuilder.swift */; }; + E65FB24C2CD524BF00BF6F56 /* InterstitialTagBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65FB2492CD524BF00BF6F56 /* 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 */; }; @@ -678,6 +681,7 @@ D4BB018C1E2EABD500CA006E /* PlaylistTagArray+RenditionGroups.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PlaylistTagArray+RenditionGroups.swift"; sourceTree = ""; }; E65FB2412CD51E4200BF6F56 /* InterstitialValueTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterstitialValueTypes.swift; sourceTree = ""; }; E65FB2452CD5241D00BF6F56 /* InterstitialValueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterstitialValueTests.swift; sourceTree = ""; }; + E65FB2492CD524BF00BF6F56 /* InterstitialTagBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterstitialTagBuilder.swift; 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 = ""; }; EC03B62F1E5CC54900BF1F97 /* RapidParser_LookingForEForEXTState_ParseArray.include */ = {isa = PBXFileReference; lastKnownFileType = text; path = RapidParser_LookingForEForEXTState_ParseArray.include; sourceTree = ""; }; @@ -1118,6 +1122,7 @@ EC7ECA011D30177A000EEB7D /* Utils */, EC7491B11DD29D5C00AF4E20 /* ValueTypes.swift */, E65FB2412CD51E4200BF6F56 /* InterstitialValueTypes.swift */, + E65FB2492CD524BF00BF6F56 /* InterstitialTagBuilder.swift */, ); path = mambaSharedFramework; sourceTree = ""; @@ -1816,6 +1821,7 @@ EC349AD62236F55F0077432B /* MasterPlaylistStructure.swift in Sources */, ECDE18442238114E008566BB /* VariantPlaylist.swift in Sources */, EC7491721DD29B5D00AF4E20 /* OrderedDictionary.swift in Sources */, + E65FB24A2CD524BF00BF6F56 /* InterstitialTagBuilder.swift in Sources */, EC3B01BF1DD4D49A00B512E3 /* PlaylistTagCardinalityValidator.swift in Sources */, EC7491FA1DD29DD300AF4E20 /* GenericSingleTagValidator.swift in Sources */, EC9547851E5CC83C00962535 /* NoOpTagParser.swift in Sources */, @@ -1995,6 +2001,7 @@ EC349AD72236F55F0077432B /* MasterPlaylistStructure.swift in Sources */, ECDE18452238114E008566BB /* VariantPlaylist.swift in Sources */, EC7491CA1DD29D5C00AF4E20 /* PlaylistWriter.swift in Sources */, + E65FB24C2CD524BF00BF6F56 /* InterstitialTagBuilder.swift in Sources */, EC3B01A61DD4D47900B512E3 /* EXT_X_KEYValidator.swift in Sources */, EC7491731DD29B5D00AF4E20 /* OrderedDictionary.swift in Sources */, EC3B01C01DD4D49A00B512E3 /* PlaylistTagCardinalityValidator.swift in Sources */, @@ -2174,6 +2181,7 @@ EC1CCD60209A2CF9006B59FF /* PlaylistValidationIssue.swift in Sources */, EC349AD82236F55F0077432B /* MasterPlaylistStructure.swift in Sources */, ECDE18462238114E008566BB /* VariantPlaylist.swift in Sources */, + E65FB24B2CD524BF00BF6F56 /* InterstitialTagBuilder.swift in Sources */, EC1CCD40209A2CF9006B59FF /* EXT_X_MEDIARenditionGroupTYPEValidator.swift in Sources */, EC1CCD21209A2CF9006B59FF /* RapidParserStateHandlers.c in Sources */, EC1CCD23209A2CF9006B59FF /* CollectionType+FindExtensions.swift in Sources */, diff --git a/mambaSharedFramework/InterstitialTagBuilder.swift b/mambaSharedFramework/InterstitialTagBuilder.swift index b05f726..349af0b 100644 --- a/mambaSharedFramework/InterstitialTagBuilder.swift +++ b/mambaSharedFramework/InterstitialTagBuilder.swift @@ -2,7 +2,7 @@ // InterstitialTagBuilder.swift // mamba // -// Created by Migneco, Ray on 11/1/24. +// 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. @@ -18,3 +18,295 @@ // import Foundation + +/// A utility class for configuring and constructing an interstitial tag +/// The properties in this class are in accordance with the HLS spec +/// outlined in `draft-pantos-hls-rfc8216bis-15` Appendix D +/// https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#appendix-D +public final class InterstitialTagBuilder { + + /// An Interstitial EXT-X-DATERANGE tag MUST have a CLASS attribute whose + /// value is "com.apple.hls.interstitial". + static let appleHLSInterstitialClassIdentifier = "com.apple.hls.interstitial" + + /// A quoted-string that uniquely identifies a Date Range in the + /// Playlist. This attribute is REQUIRED + let id: String + + /// required to be "com.apple.hls.interstitial" + let classId: String + + /// date/time at which the Date Range begins. This attribute is REQUIRED. + let startDate: Date + + /// The value of the X-ASSET-URI is a quoted-string absolute URI for a + /// single interstitial asset. An Interstitial EXT-X-DATERANGE tag + /// MUST have either the X-ASSET-URI attribute or the X-ASSET-LIST + /// attribute. It MUST NOT have both. + let assetUri: String? + + /// The value of the X-ASSET-LIST is a quoted-string URI to a JSON + /// object. + let assetList: String? + + /// the duration of the interstitial content in seconds + var duration: Double? + + /// the expected duration of the interstitial content in seconds which can indicate a value when the actual duration is not yet known + var plannedDuration: Double? + + /// The value of X-RESUME-OFFSET is a decimal-floating-point of seconds that specifies where primary playback is to resume + /// following the playback of the interstitial. + var resumeOffset: Double? + + /// The value of X-PLAYOUT-LIMIT is a decimal-floating-point of seconds that specifies a limit for the playout time of the entire interstitial. + var playoutLimit: Double? + + /// The value of the X-SNAP attribute is an enumerated-string-list of Snap Identifiers. + /// The defined Snap Identifiers are: OUT and IN. This attribute is OPTIONAL. + var alignment: HLSInterstitialAlignment? + + /// The value of the X-RESTRICT attribute is an enumerated-string-list of Navigation Restriction Identifiers. The defined Navigation + /// Restriction Identifiers are: SKIP and JUMP. These restrictions are enforced at the player UI level. + var restrictions: HLSInterstitialSeekRestrictions? + + /// This attribute indicates whether the interstitial is intended to be presented as distinct from the content ("HIGHLIGHT") or not differentiated ("PRIMARY"). + var timelineStyle: HLSInterstitialTimelineStyle? + + /// The attribute indicates whether the interstitial should be presented as a single point on the timeline or as a range. + var timelineOccupation: HLSInterstitialTimelineOccupation? + + /// Provides a hint to the client to know how coordinated playback of the same asset will behave across multiple players + var contentMayVary: Bool? + + /// The "X-" prefix defines a namespace reserved for client-defined attributes. The client-attribute MUST be a legal AttributeName. + /// Clients SHOULD use a reverse-DNS syntax when defining their own attribute names to avoid collisions. The attribute value MUST be + /// a quoted-string, a hexadecimal-sequence, or a decimal-floating- point. An example of a client-defined attribute is X-COM-EXAMPLE- + /// AD-ID="XYZ123". These attributes are OPTIONAL. + var clientAttributes: [String: LosslessStringConvertible]? + + /// Creates a Tag Builder using an asset Uri + /// + /// - Parameters: + /// - id: the identifier for the interstitial + /// - startDate: `Date` at which the interstitial begins + /// - assetUri: the URI locating the interstitial + public init(id: String, startDate: Date, assetUri: String) { + self.id = id + self.startDate = startDate + self.assetUri = assetUri + self.assetList = nil + self.classId = Self.appleHLSInterstitialClassIdentifier + } + + /// Creates a Tag Builder using an Asset List Uri + /// + /// - Parameters: + /// - id: the identifier for the interstitial + /// - startDate: `Date` indicating when the interstitial begins + /// - assetList: the URI to a JSON object containing the assets + public init(id: String, startDate: Date, assetList: String) { + self.id = id + self.startDate = startDate + self.assetList = assetList + self.assetUri = nil + self.classId = Self.appleHLSInterstitialClassIdentifier + } + + /// Specifies the duration of the interstitial + /// + /// - Parameter duration: `Double` indicating duration + /// + /// - Returns: an instance of the builder + @discardableResult + public func withDuration(_ duration: Double) -> Self { + self.duration = duration + + return self + } + + /// Specifies the planned duration of the interstitial + /// + /// - Parameter duration: `Double` indicating duration + /// + /// - Returns: an instance of the builder + @discardableResult + public func withPlannedDuration(_ plannedDuration: Double) -> Self { + self.plannedDuration = plannedDuration + + return self + } + + /// Configures the interstitial with a resume offset + /// + /// - Parameter offset: `Double` indicating the resume offset + /// + /// - Returns: an instance of the builder + @discardableResult + public func withResumeOffset(_ offset: Double) -> Self { + self.resumeOffset = offset + + return self + } + + /// Configures the interstitial with a playout limit + /// + /// - Parameter limit: `Double` indicating playout limit + /// + /// - Returns: an instance of the builder + @discardableResult + public func withPlayoutLimit(_ limit: Double) -> Self { + self.playoutLimit = limit + + return self + } + + /// Specifies the alignment of the interstitial with respect to content + /// + /// - Parameter alignment: `HLSInterstitialAlignment` specifying alignment guides + /// + /// - Returns: an instance of the builder + @discardableResult + public func withAlignment(_ alignment: HLSInterstitialAlignment) -> Self { + self.alignment = alignment + + return self + } + + /// Specifies seek restrictions applied to the interstitial + /// + /// - Parameter restrictions: instance of `HLSInterstitialSeekRestrictions` + /// + /// - Returns: an instance of the builder + public func withRestrictions(_ restrictions: HLSInterstitialSeekRestrictions) -> Self { + self.restrictions = restrictions + + return self + } + + /// Specifies how the interstitial is styled on the timeline + /// + /// - Parameter style: `HLSInterstitialTimelineStyle` type + /// + /// - Returns: an instance of the builder + @discardableResult + public func withTimelineStyle(_ style: HLSInterstitialTimelineStyle) -> Self { + self.timelineStyle = style + + return self + } + + /// Describes how the interstitial occupies the content timeline + /// + /// - Parameter occupation: `HLSInterstitialTimelineOccupation` type + /// + /// - Returns: an instance of the builder + @discardableResult + public func withTimelineOccupation(_ occupation: HLSInterstitialTimelineOccupation) -> Self { + self.timelineOccupation = occupation + + return self + } + + /// Indicates if the interstitial content varies or stays the same during a shared watching activity + /// + /// - Parameter variation: `Bool` indicating if there's variation + /// + /// - Returns: an instance of the builder + @discardableResult + public func withContentVariation(_ variation: Bool) -> Self { + self.contentMayVary = variation + + return self + } + + /// Specifies client attributes describing the interstitial + /// + /// - Parameter attributes: a map of `[String: LosslessStringConvertible]` describing the attributes + /// + /// - Returns: an instance of the builder + @discardableResult + public func withClientAttributes(_ attributes: [String: LosslessStringConvertible]) -> Self { + self.clientAttributes = attributes + + return self + } + + /// Builds the DateRange tag utilizing the configured HLS interstitial properties + /// + /// - Returns: `PlaylistTag` + public func buildTag() -> PlaylistTag { + + var tagDictionary = PlaylistTagDictionary() + + tagDictionary[PantosValue.id.rawValue] = PlaylistTagValueData(value: id, quoteEscaped: true) + let startDateString = String.DateFormatter.iso8601MS.string(from: startDate) + tagDictionary[PantosValue.startDate.rawValue] = PlaylistTagValueData(value: startDateString, + quoteEscaped: true) + tagDictionary[PantosValue.classAttribute.rawValue] = PlaylistTagValueData(value: classId, + quoteEscaped: true) + + if let assetUri { + tagDictionary[PantosValue.assetUri.rawValue] = PlaylistTagValueData(value: assetUri, quoteEscaped: true) + } + + if let assetList { + tagDictionary[PantosValue.assetList.rawValue] = PlaylistTagValueData(value: assetList, quoteEscaped: true) + } + + if let duration { + tagDictionary[PantosValue.duration.rawValue] = PlaylistTagValueData(value: String(duration), + quoteEscaped: false) + } + + if let plannedDuration { + tagDictionary[PantosValue.plannedDuration.rawValue] = PlaylistTagValueData(value: String(plannedDuration), + quoteEscaped: false) + } + + if let resumeOffset { + tagDictionary[PantosValue.resumeOffset.rawValue] = PlaylistTagValueData(value: String(resumeOffset), + quoteEscaped: false) + } + + if let playoutLimit { + tagDictionary[PantosValue.playoutLimit.rawValue] = PlaylistTagValueData(value: String(playoutLimit), + quoteEscaped: false) + } + + if let restrictions { + let str = restrictions.restrictions.map({ $0.rawValue }).joined(separator: ",") + tagDictionary[PantosValue.restrict.rawValue] = PlaylistTagValueData(value: str, quoteEscaped: true) + } + + if let alignment { + let str = alignment.values.map({ $0.rawValue }).joined(separator: ",") + tagDictionary[PantosValue.snap.rawValue] = PlaylistTagValueData(value: str, quoteEscaped: true) + } + + if let timelineStyle { + tagDictionary[PantosValue.timelineStyle.rawValue] = PlaylistTagValueData(value: timelineStyle.rawValue, + quoteEscaped: true) + } + + if let timelineOccupation { + tagDictionary[PantosValue.timelineOccupies.rawValue] = PlaylistTagValueData(value: timelineOccupation.rawValue, + quoteEscaped: true) + } + + if let contentMayVary { + tagDictionary[PantosValue.contentMayVary.rawValue] = PlaylistTagValueData(value: contentMayVary == true ? "YES" : "NO", + quoteEscaped: true) + } + + if let clientAttributes { + for (k, v) in clientAttributes { + tagDictionary[k] = PlaylistTagValueData(value: String(v), quoteEscaped: true) + } + } + + return PlaylistTag(tagDescriptor: PantosTag.EXT_X_DATERANGE, + stringTagData: nil, + parsedValues: tagDictionary) + } +}