From 9f03dc7b61a8d4572961e3ad72d766fac9031fa4 Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Sat, 31 Aug 2024 20:37:00 -0400 Subject: [PATCH 01/18] Adding multivariant playlist tags defined in draft 15 and still missing here --- mamba.xcodeproj/project.pbxproj | 26 ++- mambaSharedFramework/HLSValidationIssue.swift | 3 + .../EXT_X_SESSION_DATAPlaylistValidator.swift | 51 +++++ .../EXT_X_SESSION_DATATagValidator.swift | 71 ++++++ .../EXT_X_SESSION_KEYValidator.swift | 42 ++++ .../HLSPlaylistValidatorImpl.swift | 3 +- .../PantosTag.swift | 66 +++++- .../PantosValue.swift | 21 +- mambaTests/HLSValidatorTests.swift | 26 ++- mambaTests/PantosTagTests.swift | 9 + .../GenericDictionaryTagValidatorTests.swift | 207 +++++++++++++++++- 11 files changed, 516 insertions(+), 9 deletions(-) create mode 100644 mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift create mode 100644 mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift create mode 100644 mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift diff --git a/mamba.xcodeproj/project.pbxproj b/mamba.xcodeproj/project.pbxproj index e0d3d3f..1f68096 100644 --- a/mamba.xcodeproj/project.pbxproj +++ b/mamba.xcodeproj/project.pbxproj @@ -8,6 +8,15 @@ /* Begin PBXBuildFile section */ 01CD2E7A1DE4D46F002510E7 /* EXT_X_MAPTagParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CD2E791DE4D46F002510E7 /* EXT_X_MAPTagParserTests.swift */; }; + 1447582D2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1447582C2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift */; }; + 1447582E2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1447582C2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift */; }; + 1447582F2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1447582C2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift */; }; + 144758312C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758302C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift */; }; + 144758322C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758302C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift */; }; + 144758332C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758302C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift */; }; + 144758352C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758342C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift */; }; + 144758362C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758342C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift */; }; + 144758372C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758342C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift */; }; 1D28F3451EAA9E500010320B /* hls_ad_master_playlist.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 1D28F3401EAA9E500010320B /* hls_ad_master_playlist.m3u8 */; }; 1D28F3461EAA9E500010320B /* hls_ad_variant_playlist.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 1D28F3411EAA9E500010320B /* hls_ad_variant_playlist.m3u8 */; }; 1D28F3471EAA9E500010320B /* hls_master_playlist_sap.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 1D28F3421EAA9E500010320B /* hls_master_playlist_sap.m3u8 */; }; @@ -614,6 +623,9 @@ /* Begin PBXFileReference section */ 01CD2E791DE4D46F002510E7 /* EXT_X_MAPTagParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EXT_X_MAPTagParserTests.swift; sourceTree = ""; }; + 1447582C2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXT_X_SESSION_KEYValidator.swift; sourceTree = ""; }; + 144758302C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXT_X_SESSION_DATATagValidator.swift; sourceTree = ""; }; + 144758342C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXT_X_SESSION_DATAPlaylistValidator.swift; sourceTree = ""; }; 1D28F3401EAA9E500010320B /* hls_ad_master_playlist.m3u8 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = hls_ad_master_playlist.m3u8; sourceTree = ""; }; 1D28F3411EAA9E500010320B /* hls_ad_variant_playlist.m3u8 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = hls_ad_variant_playlist.m3u8; sourceTree = ""; }; 1D28F3421EAA9E500010320B /* hls_master_playlist_sap.m3u8 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = hls_master_playlist_sap.m3u8; sourceTree = ""; }; @@ -900,7 +912,6 @@ 0173AB0D1D5BB371005DE51B /* Pantos-Generic Tag Validators */ = { isa = PBXGroup; children = ( - EC95478A1E5CC86300962535 /* EXTINFValidator.swift */, 6DD0A1B0242FADC600FF7AAE /* EXT_X_DATERANGEPlaylistValidator.swift */, 6DD0A1AC242F85C800FF7AAE /* EXT_X_DATERANGETagValidator.swift */, EC3B019E1DD4D47900B512E3 /* EXT_X_KEYValidator.swift */, @@ -909,9 +920,13 @@ EC3B01A11DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupNAMEValidator.swift */, EC3B01A21DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupTYPEValidator.swift */, 43DE4EFC1E564DBE00EEE800 /* EXT_X_MEDIARenditionINSTREAMIDValidator.swift */, + 144758342C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift */, + 144758302C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift */, + 1447582C2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift */, 43DE4EFA1E564DA300EEE800 /* EXT_X_STARTTimeOffsetValidator.swift */, EC3B01A31DD4D47900B512E3 /* EXT_X_STREAM_INFRenditionGroupValidator.swift */, EC3B01A41DD4D47900B512E3 /* EXT_X_TARGETDURATIONLengthValidator.swift */, + EC95478A1E5CC86300962535 /* EXTINFValidator.swift */, EC7491F51DD29DD300AF4E20 /* GenericDictionaryTagValidator.swift */, EC7491F61DD29DD300AF4E20 /* GenericSingleTagValidator.swift */, EC7491F71DD29DD300AF4E20 /* HLSDictionaryTagValueIdentifier.swift */, @@ -1761,6 +1776,7 @@ ECFAA6581E6DD93C00398D66 /* HLSPlaylist.swift in Sources */, ECC410601EA02F4800B4E3C8 /* StructureState.swift in Sources */, EC7491811DD29C3500AF4E20 /* String+Trim.swift in Sources */, + 144758352C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */, EC7491C31DD29D5C00AF4E20 /* HLSValidationIssue.swift in Sources */, EC74916E1DD29B5D00AF4E20 /* CollectionType+FindExtensions.swift in Sources */, EC7491DA1DD29D9600AF4E20 /* GenericNoDataTagParser.swift in Sources */, @@ -1830,7 +1846,9 @@ EC3B01AB1DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupNAMEValidator.swift in Sources */, EC7491651DD29B0F00AF4E20 /* FailableStringLiteralConvertible.swift in Sources */, EC7491461DD299B400AF4E20 /* HLSPlaylistTypes.swift in Sources */, + 144758312C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift in Sources */, EC3B01A71DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupAUTOSELECTValidator.swift in Sources */, + 1447582D2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift in Sources */, F700CD391E78A2BE001C9487 /* HLSStringRef_ConcreteNSString.m in Sources */, 43DE4EFB1E564DA300EEE800 /* EXT_X_STARTTimeOffsetValidator.swift in Sources */, EC74918A1DD29CCB00AF4E20 /* StringDictionaryParser.swift in Sources */, @@ -1926,6 +1944,7 @@ ECFAA6591E6DD93C00398D66 /* HLSPlaylist.swift in Sources */, ECC410611EA02F4800B4E3C8 /* StructureState.swift in Sources */, EC3B01AA1DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupDEFAULTValidator.swift in Sources */, + 144758362C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */, EC3B01C41DD4D49A00B512E3 /* HLSPlaylistOneToManyValidator.swift in Sources */, EC7491821DD29C3500AF4E20 /* String+Trim.swift in Sources */, EC7491C41DD29D5C00AF4E20 /* HLSValidationIssue.swift in Sources */, @@ -1995,7 +2014,9 @@ EC74917E1DD29C3500AF4E20 /* String+DateParsing.swift in Sources */, EC3B01AC1DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupNAMEValidator.swift in Sources */, EC7491661DD29B0F00AF4E20 /* FailableStringLiteralConvertible.swift in Sources */, + 144758322C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift in Sources */, EC7491471DD299B400AF4E20 /* HLSPlaylistTypes.swift in Sources */, + 1447582E2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift in Sources */, F700CD3A1E78A2BE001C9487 /* HLSStringRef_ConcreteNSString.m in Sources */, EC3B01A81DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupAUTOSELECTValidator.swift in Sources */, EC74918B1DD29CCB00AF4E20 /* StringDictionaryParser.swift in Sources */, @@ -2091,6 +2112,7 @@ EC1CCD36209A2CF9006B59FF /* URL+hlsplaylist.swift in Sources */, EC1CCD32209A2CF9006B59FF /* String+Trim.swift in Sources */, EC1CCD46209A2CF9006B59FF /* GenericSingleTagValidator.swift in Sources */, + 144758372C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */, EC1CCD59209A2CF9006B59FF /* HLSParser.swift in Sources */, EC1CCD30209A2CF9006B59FF /* String+DateParsing.swift in Sources */, EC1CCD53209A2CF9006B59FF /* GenericDictionaryTagWriter.swift in Sources */, @@ -2160,7 +2182,9 @@ EC1CCD4F209A2CF9006B59FF /* HLSPlaylistTagCardinalityValidation.swift in Sources */, EC1CCD2B209A2CF9006B59FF /* IndeterminateBool.swift in Sources */, EC1CCCF9209A2CF9006B59FF /* HLSTagCriterion.swift in Sources */, + 144758332C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift in Sources */, EC1CCD43209A2CF9006B59FF /* EXT_X_STREAM_INFRenditionGroupValidator.swift in Sources */, + 1447582F2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift in Sources */, EC1CCD35209A2CF9006B59FF /* StringDictionaryParser.swift in Sources */, EC1CCD02209A2CF9006B59FF /* HLSStringRef_ConcreteNSString.m in Sources */, EC1CCD38209A2CF9006B59FF /* GenericNoDataTagParser.swift in Sources */, diff --git a/mambaSharedFramework/HLSValidationIssue.swift b/mambaSharedFramework/HLSValidationIssue.swift index 36414c6..0ca620c 100644 --- a/mambaSharedFramework/HLSValidationIssue.swift +++ b/mambaSharedFramework/HLSValidationIssue.swift @@ -48,6 +48,9 @@ public enum IssueDescription: String { case HLSPlaylistRenditionGroupMatchingNAMELANGUAGEValidator = "A Playlist MAY contain multiple groups of the same TYPE in order to provide multiple encodings of each rendition. If it does so, each group of the same TYPE SHOULD contain corresponding members with the same NAME attribute, LANGUAGE attribute, and rendition." case EXT_X_KEYValidator = "EXT-X-KEY If the encryption method is NONE, the URI, IV, KEYFORMAT and KEYFORMATVERSIONS attributes MUST NOT be present. If the encryption method is AES-128 or SAMPLE-AES, the URI attribute MUST be present." + case EXT_X_SESSION_KEYValidator = "All attributes defined for the EXT-X-KEY tag are also defined for the EXT-X-SESSION-KEY, except that the value of the METHOD attribute MUST NOT be NONE." + case EXT_X_SESSION_DATATagValidator = "Each EXT-X-SESSION-DATA tag MUST contain either a VALUE or URI attribute, but not both." + case EXT_X_SESSION_DATAPlaylistValidator = "A Playlist MAY contain multiple EXT-X-SESSION-DATA tags with the same DATA-ID attribute. A Playlist MUST NOT contain more than one EXT-X-SESSION-DATA tag with the same DATA-ID attribute and the same LANGUAGE attribute." case HLSPlaylistRenditionGroupMatchingPROGRAM_IDValidator = "Variant Playlists MUST contain an EXT-X-STREAM-INF tag or EXT-X-I-FRAME-STREAM-INF tag for each variant stream. Each tag identifying an encoding of the same presentation MUST have the same PROGRAM-ID attribute value." case EXT_X_STREAM_INFRenditionGroupAUDIOValidator = "EXT-X-STREAM-INF - AUDIO The value is a quoted-string. It MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Playlist whose TYPE attribute is AUDIO." case EXT_X_STREAM_INFRenditionGroupVIDEOValidator = "EXT-X-STREAM-INF - VIDEO The value is a quoted-string. It MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Playlist whose TYPE attribute is VIDEO." diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift new file mode 100644 index 0000000..79ad76c --- /dev/null +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift @@ -0,0 +1,51 @@ +// +// EXT_X_SESSION_DATAPlaylistValidator.swift +// mamba +// +// Created by Robert Galluccio on 8/31/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 + +class EXT_X_SESSION_DATAPlaylistValidator: HLSPlaylistValidator { + static func validate(hlsPlaylist: any HLSPlaylistInterface) -> [HLSValidationIssue]? { + var issues = [HLSValidationIssue]() + + let issue = duplicateIssue(tags: hlsPlaylist.tags.filter { $0.tagDescriptor == PantosTag.EXT_X_SESSION_DATA }) + if let issue { + issues.append(issue) + } + + return issues.isEmpty ? nil : issues + } + + // A Playlist MAY contain multiple EXT-X-SESSION-DATA tags with the same DATA-ID attribute. A Playlist MUST NOT + // contain more than one EXT-X-SESSION-DATA tag with the same DATA-ID attribute and the same LANGUAGE attribute. + private static func duplicateIssue(tags: [HLSTag]) -> HLSValidationIssue? { + var dataIdToLanguagesMap = [String: [String?]]() + for tag in tags { + guard let dataId = tag.value(forValueIdentifier: PantosValue.dataId) else { continue } + var existingLanguages = dataIdToLanguagesMap[dataId] ?? [] + existingLanguages.append(tag.value(forValueIdentifier: PantosValue.language)) + dataIdToLanguagesMap[dataId] = existingLanguages + } + for languages in dataIdToLanguagesMap.values { + if languages.count != Set(languages).count { + return HLSValidationIssue(description: .EXT_X_SESSION_DATAPlaylistValidator, severity: .error) + } + } + return nil + } +} diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift new file mode 100644 index 0000000..f3d4d70 --- /dev/null +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift @@ -0,0 +1,71 @@ +// +// EXT_X_SESSION_DATATagValidator.swift +// mamba +// +// Created by Robert Galluccio on 8/31/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 + +class EXT_X_SESSION_DATATagValidator: HLSTagValidator { + private var genericValidator: GenericDictionaryTagValidator + + init() { + genericValidator = GenericDictionaryTagValidator( + tag: PantosTag.EXT_X_SESSION_DATA, + dictionaryValueIdentifiers: [ + HLSDictionaryTagValueIdentifierImpl( + valueId: PantosValue.dataId, + optional: false, + expectedType: String.self + ), + HLSDictionaryTagValueIdentifierImpl( + valueId: PantosValue.value, + optional: true, + expectedType: String.self + ), + HLSDictionaryTagValueIdentifierImpl( + valueId: PantosValue.uri, + optional: true, + expectedType: String.self + ), + HLSDictionaryTagValueIdentifierImpl( + valueId: PantosValue.format, + optional: true, + expectedType: String.self + ), + HLSDictionaryTagValueIdentifierImpl( + valueId: PantosValue.language, + optional: true, + expectedType: String.self + ), + ] + ) + } + + func validate(tag: HLSTag) -> [HLSValidationIssue]? { + var issueList = genericValidator.validate(tag: tag) ?? [] + + // Each EXT-X-SESSION-DATA tag MUST contain either a VALUE or URI attribute, but not both. + switch (tag.value(forValueIdentifier: PantosValue.value), tag.value(forValueIdentifier: PantosValue.uri)) { + case (.none, .some), (.some, .none): + break + case (.some, .some), (.none, .none): + issueList.append(HLSValidationIssue(description: .EXT_X_SESSION_DATATagValidator, severity: .error)) + } + + return issueList.isEmpty ? nil : issueList + } +} diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift new file mode 100644 index 0000000..e814e72 --- /dev/null +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift @@ -0,0 +1,42 @@ +// +// EXT_X_SESSION_KEYValidator.swift +// mamba +// +// Created by Robert Galluccio on 8/31/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 + +// All attributes defined for the EXT-X-KEY tag (Section 4.4.4.4) are also defined for the +// EXT-X-SESSION-KEY, except that the value of the METHOD attribute MUST NOT be NONE. +class EXT_X_SESSION_KEYValidator: EXT_X_KEYValidator { + + override public func validate(tag: HLSTag) -> [HLSValidationIssue]? { + var issueList = super.validate(tag: tag) ?? [] + + if let method = tag.value(forValueIdentifier: PantosValue.method) { + if method == HLSEncryptionMethodType.EncryptionMethod.None.rawValue { + issueList.append( + HLSValidationIssue( + description: IssueDescription.EXT_X_SESSION_KEYValidator, + severity: IssueSeverity.error + ) + ) + } + } + + return issueList.isEmpty ? nil : issueList + } +} diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/HLSPlaylistValidatorImpl.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/HLSPlaylistValidatorImpl.swift index 5582911..6a3d134 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/HLSPlaylistValidatorImpl.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/HLSPlaylistValidatorImpl.swift @@ -86,7 +86,8 @@ public class HLSVariantPlaylistValidator: HLSExtensibleValidator { HLSPlaylistRenditionGroupMatchingPROGRAM_IDValidator.self, HLSPlaylistRenditionGroupMatchingNAMELANGUAGEValidator.self, EXT_X_STARTTimeOffsetValidator.self, - EXT_X_DATERANGEPlaylistValidator.self] + EXT_X_DATERANGEPlaylistValidator.self, + EXT_X_SESSION_DATAPlaylistValidator.self] } /// A general purpose validator that will validate either a variant or a master playlist diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift index a2b19a6..38f6cd9 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift @@ -54,7 +54,10 @@ public enum PantosTag: String { case EXT_X_MEDIA = "EXT-X-MEDIA" case EXT_X_STREAM_INF = "EXT-X-STREAM-INF" case EXT_X_I_FRAME_STREAM_INF = "EXT-X-I-FRAME-STREAM-INF" - + case EXT_X_SESSION_DATA = "EXT-X-SESSION-DATA" + case EXT_X_SESSION_KEY = "EXT-X-SESSION-KEY" + case EXT_X_CONTENT_STEERING = "EXT-X-CONTENT-STEERING" + // MARK: Variant playlist tags case EXT_X_TARGETDURATION = "EXT-X-TARGETDURATION" case EXT_X_MEDIA_SEQUENCE = "EXT-X-MEDIA-SEQUENCE" @@ -128,6 +131,12 @@ extension PantosTag: HLSTagDescriptor, Equatable { fallthrough case .EXT_X_I_FRAME_STREAM_INF: fallthrough + case .EXT_X_SESSION_DATA: + fallthrough + case .EXT_X_SESSION_KEY: + fallthrough + case .EXT_X_CONTENT_STEERING: + fallthrough case .EXT_X_ENDLIST: fallthrough case .EXT_X_INDEPENDENT_SEGMENTS: @@ -193,6 +202,12 @@ extension PantosTag: HLSTagDescriptor, Equatable { case .EXT_X_I_FRAME_STREAM_INF: fallthrough + case .EXT_X_SESSION_DATA: + fallthrough + case .EXT_X_SESSION_KEY: + fallthrough + case .EXT_X_CONTENT_STEERING: + fallthrough case .EXT_X_MEDIA: fallthrough case .EXT_X_STREAM_INF: @@ -271,6 +286,12 @@ extension PantosTag: HLSTagDescriptor, Equatable { fallthrough case .EXT_X_I_FRAME_STREAM_INF: fallthrough + case .EXT_X_SESSION_DATA: + fallthrough + case .EXT_X_SESSION_KEY: + fallthrough + case .EXT_X_CONTENT_STEERING: + fallthrough case .EXT_X_MAP: fallthrough case .EXT_X_START: @@ -335,6 +356,12 @@ extension PantosTag: HLSTagDescriptor, Equatable { fallthrough case .EXT_X_I_FRAME_STREAM_INF: fallthrough + case .EXT_X_SESSION_DATA: + fallthrough + case .EXT_X_SESSION_KEY: + fallthrough + case .EXT_X_CONTENT_STEERING: + fallthrough case .EXT_X_MAP: fallthrough case .EXT_X_START: @@ -432,7 +459,39 @@ extension PantosTag: HLSTagDescriptor, Equatable { HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.uri, optional: false, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoGroup, optional: true, expectedType: String.self), ]) - + + case .EXT_X_SESSION_DATA: + return EXT_X_SESSION_DATATagValidator() + + case .EXT_X_SESSION_KEY: + return EXT_X_SESSION_KEYValidator(tag: pantostag, dictionaryValueIdentifiers: [ + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.method, + optional: false, + expectedType: HLSEncryptionMethodType.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.uri, + optional: false, // URI is REQUIRED since METHOD can't be NONE + expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.ivector, + optional: true, + expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.keyformat, + optional: true, + expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.keyformatVersions, + optional: true, + expectedType: String.self) + ]) + + case .EXT_X_CONTENT_STEERING: + return GenericDictionaryTagValidator(tag: pantostag, dictionaryValueIdentifiers: [ + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.serverUri, + optional: false, + expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.pathwayId, + optional: true, + expectedType: String.self) + ]) + case .EXT_X_KEY: return EXT_X_KEYValidator(tag: pantostag, dictionaryValueIdentifiers: [ HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.method, optional: true, expectedType: HLSEncryptionMethodType.self), @@ -493,6 +552,9 @@ extension PantosTag: HLSTagDescriptor, Equatable { PantosTag.EXT_X_VERSION, PantosTag.EXT_X_MEDIA, PantosTag.EXT_X_I_FRAME_STREAM_INF, + PantosTag.EXT_X_SESSION_DATA, + PantosTag.EXT_X_SESSION_KEY, + PantosTag.EXT_X_CONTENT_STEERING, PantosTag.EXT_X_TARGETDURATION, PantosTag.EXT_X_MEDIA_SEQUENCE, PantosTag.EXT_X_ENDLIST, diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosValue.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosValue.swift index 3fc799b..350cb47 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosValue.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosValue.swift @@ -57,7 +57,22 @@ public enum PantosValue: String { /// Found in `.EXT_X_STREAM_INF`. Match a tag with a corresponding closed-caption stream. case closedCaptionsGroup = "CLOSED-CAPTIONS" - + + /// Found in `.EXT_X_SESSION_DATA`. Identifier for a particular data value. + case dataId = "DATA-ID" + + /// Found in `.EXT_X_SESSION_DATA`. The value of the data identified via DATA-ID. + case value = "VALUE" + + /// Found in `.EXT_X_SESSION_DATA`. The format of the data provided via VALUE. + case format = "FORMAT" + + /// Found in `.EXT_X_CONTENT_STEERING`. The URI location for the steering manifest. + case serverUri = "SERVER-URI" + + /// Found in `.EXT_X_CONTENT_STEERING`. The initial pathway to choose until the first steering manifest is obtained. + case pathwayId = "PATHWAY-ID" + /// Found in `.EXT_X_TARGETDURATION`. A target duration in seconds. case targetDurationSeconds = "targetDurationSeconds" @@ -79,7 +94,7 @@ public enum PantosValue: String { /// Found in `.EXT_X_MEDIA`. Name of this media (typically a human-readable version of the language) case name = "NAME" - /// Found in `.EXT_X_MEDIA`. The primary language of the media + /// Found in `.EXT_X_MEDIA` and `.EXT_X_SESSION_DATA`. The primary language of the media case language = "LANGUAGE" /// Found in `.EXT_X_MEDIA`. The associated language of the media @@ -100,7 +115,7 @@ public enum PantosValue: String { /// Found in `.EXT_X_MEDIA`. This attribute is REQUIRED if the TYPE attribute is CLOSED-CAPTIONS ("CC1", "CC2", "CC3", "CC4") case instreamId = "INSTREAM-ID" - /// Found in `.EXT_X_MEDIA`, `.EXT_X_KEY`, `.EXT_X_MAP` and `.EXT_X_I_FRAME_STREAM_INF`. The URI location of the media + /// Found in `.EXT_X_MEDIA`, `.EXT_X_KEY`, `.EXT_X_MAP`, `.EXT_X_I_FRAME_STREAM_INF` and `.EXT_X_SESSION_DATA`. The URI location of the media case uri = "URI" /// Found in `.EXT_X_KEY`. The encryption method diff --git a/mambaTests/HLSValidatorTests.swift b/mambaTests/HLSValidatorTests.swift index eecd81c..cd7b635 100644 --- a/mambaTests/HLSValidatorTests.swift +++ b/mambaTests/HLSValidatorTests.swift @@ -785,5 +785,29 @@ frag1.ts let expectedIssues = [HLSValidationIssue(description: .EXT_X_DATERANGEAttributeMismatchForTagsWithSameID, severity: .warning)] validate(validator: u, playlist: hlsLoadString, expectedIssues: expectedIssues) } - + + func testEXT_X_SESSION_DATAPlaylistValidator_multipleSessionDataDifferentLanguageIsOK() { + var tags = EXT_X_MEDIA_txt + tags.insert("#EXT-X-SESSION-DATA:DATA-ID=\"com.example.text\",VALUE=\"example\",LANGUAGE=\"en\"\n", at: 1) + tags.insert("#EXT-X-SESSION-DATA:DATA-ID=\"com.example.text\",VALUE=\"example\",LANGUAGE=\"es\"\n", at: 1) + let playlist = tags.joined() + validate( + validator: EXT_X_SESSION_DATAPlaylistValidator.self, + playlist: playlist, + expectedIssues: [] + ) + } + + func testEXT_X_SESSION_DATAPlaylistValidator_multipleSessionDataSameLanguageIsNotOK() { + var tags = EXT_X_MEDIA_txt + tags.insert("#EXT-X-SESSION-DATA:DATA-ID=\"com.example.text\",VALUE=\"example\",LANGUAGE=\"en\"\n", at: 1) + tags.insert("#EXT-X-SESSION-DATA:DATA-ID=\"com.example.text\",VALUE=\"example\",LANGUAGE=\"en\"\n", at: 1) + let playlist = tags.joined() + validate( + validator: EXT_X_SESSION_DATAPlaylistValidator.self, + playlist: playlist, + expectedIssues: [HLSValidationIssue(description: .EXT_X_SESSION_DATAPlaylistValidator, severity: .error)] + ) + } + } diff --git a/mambaTests/PantosTagTests.swift b/mambaTests/PantosTagTests.swift index 69c1850..cdc0560 100644 --- a/mambaTests/PantosTagTests.swift +++ b/mambaTests/PantosTagTests.swift @@ -33,6 +33,9 @@ class PantosTagTests: XCTestCase { runStringRefLookupTest(onPantosDescriptor: PantosTag.EXT_X_KEY) runStringRefLookupTest(onPantosDescriptor: PantosTag.EXTM3U) runStringRefLookupTest(onPantosDescriptor: PantosTag.EXT_X_I_FRAMES_ONLY) + runStringRefLookupTest(onPantosDescriptor: PantosTag.EXT_X_SESSION_DATA) + runStringRefLookupTest(onPantosDescriptor: PantosTag.EXT_X_SESSION_KEY) + runStringRefLookupTest(onPantosDescriptor: PantosTag.EXT_X_CONTENT_STEERING) runStringRefLookupTest(onPantosDescriptor: PantosTag.EXT_X_MEDIA_SEQUENCE) runStringRefLookupTest(onPantosDescriptor: PantosTag.EXT_X_ALLOW_CACHE) @@ -74,6 +77,12 @@ class PantosTagTests: XCTestCase { fallthrough case .EXT_X_I_FRAMES_ONLY: fallthrough + case .EXT_X_SESSION_DATA: + fallthrough + case .EXT_X_SESSION_KEY: + fallthrough + case .EXT_X_CONTENT_STEERING: + fallthrough case .EXT_X_MEDIA_SEQUENCE: fallthrough case .EXT_X_ALLOW_CACHE: diff --git a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index 3293895..f940aab 100644 --- a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift +++ b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift @@ -515,7 +515,212 @@ class GenericDictionaryTagValidatorTests: XCTestCase { mandatory: mandatory, badValues: badValues) } - + + /* + The EXT-X-SESSION-DATA tag allows arbitrary session data to be + carried in a Multivariant Playlist. + + Its format is: + + #EXT-X-SESSION-DATA: + + The following attributes are defined: + + DATA-ID + + The value of DATA-ID is a quoted-string that identifies a + particular data value. The DATA-ID SHOULD conform to a reverse + DNS naming convention, such as "com.example.movie.title"; however, + there is no central registration authority, so Playlist authors + SHOULD take care to choose a value that is unlikely to collide + with others. This attribute is REQUIRED. + + VALUE + + VALUE is a quoted-string. It contains the data identified by + DATA-ID. If the LANGUAGE is specified, VALUE SHOULD contain a + human-readable string written in the specified language. + + URI + + The value is a quoted-string containing a URI. The resource + identified by the URI MUST be formatted as indicated by the FORMAT + attribute; otherwise, clients may fail to interpret the resource. + + FORMAT + + The value is an enumerated-string; valid strings are JSON and RAW. + The FORMAT attribute MUST be ignored when URI attribute is + missing. + + If the value is JSON, the URI MUST reference a JSON [RFC8259] + format file. If the value is RAW, the URI SHALL be treated as a + binary file. + + This attribute is OPTIONAL. Its absence implies a value of JSON. + + LANGUAGE + + The value is a quoted-string containing a language tag [RFC5646] + that identifies the language of the VALUE. This attribute is + OPTIONAL. + + Each EXT-X-SESSION-DATA tag MUST contain either a VALUE or URI + attribute, but not both. + + A Playlist MAY contain multiple EXT-X-SESSION-DATA tags with the same + DATA-ID attribute. A Playlist MUST NOT contain more than one EXT-X- + SESSION-DATA tag with the same DATA-ID attribute and the same + LANGUAGE attribute. + */ + func test_EXT_X_SESSION_DATA() { + let withURI = "DATA-ID=\"com.example.data\",URI=\"http://not.a.server/data.txt\",FORMAT=RAW,LANGUAGE=\"en\"" + validate(tag: PantosTag.EXT_X_SESSION_DATA, + tagData: withURI, + optional: [.value, .format, .language], + mandatory: [.dataId, .uri], + badValues: []) + + let withValue = "DATA-ID=\"com.example.data\",VALUE=\"Hello, World!\",LANGUAGE=\"en\"" + validate(tag: PantosTag.EXT_X_SESSION_DATA, + tagData: withValue, + optional: [.uri, .format, .language], + mandatory: [.dataId, .value], + badValues: []) + + // Using a closure to avoid naming clashes in the rest of the test. + let EXT_X_SESSION_DATA_withNoValueOrURI = { + let tagData = "DATA-ID=\"com.example.data\"" + let tag = createHLSTag(tagDescriptor: PantosTag.EXT_X_SESSION_DATA, tagData: tagData) + guard let validator = PantosTag.validator(forTag: PantosTag.EXT_X_SESSION_DATA) else { + return XCTFail("No validator for EXT-X-SESSION-DATA") + } + guard let issues = validator.validate(tag: tag) else { + return XCTFail("Should have issues when validating EXT-X-SESSION-DATA with no VALUE nor URI.") + } + XCTAssertEqual(1, issues.count, "Should have one issue") + guard let issue = issues.first else { return XCTFail("Should have at least one issue") } + XCTAssertEqual(issue.description, IssueDescription.EXT_X_SESSION_DATATagValidator.rawValue) + XCTAssertEqual(issue.severity, .error) + } + EXT_X_SESSION_DATA_withNoValueOrURI() + + let EXT_X_SESSION_DATA_withValueAndURI = { + let tagData = "DATA-ID=\"com.example.data\",VALUE=\"example\",URI=\"http://not.a.server/example\"" + let tag = createHLSTag(tagDescriptor: PantosTag.EXT_X_SESSION_DATA, tagData: tagData) + guard let validator = PantosTag.validator(forTag: PantosTag.EXT_X_SESSION_DATA) else { + return XCTFail("No validator for EXT-X-SESSION-DATA") + } + guard let issues = validator.validate(tag: tag) else { + return XCTFail("Should have issues when validating EXT-X-SESSION-DATA with both VALUE and URI.") + } + XCTAssertEqual(1, issues.count, "Should have one issue") + guard let issue = issues.first else { return XCTFail("Should have at least one issue") } + XCTAssertEqual(issue.description, IssueDescription.EXT_X_SESSION_DATATagValidator.rawValue) + XCTAssertEqual(issue.severity, .error) + } + EXT_X_SESSION_DATA_withValueAndURI() + } + + /* + The EXT-X-SESSION-KEY tag allows encryption keys from Media Playlists + to be specified in a Multivariant Playlist. This allows the client + to preload these keys without having to read the Media Playlist(s) + first. + + Its format is: + + #EXT-X-SESSION-KEY: + + All attributes defined for the EXT-X-KEY tag (Section 4.4.4.4) are + also defined for the EXT-X-SESSION-KEY, except that the value of the + METHOD attribute MUST NOT be NONE. If an EXT-X-SESSION-KEY is used, + the values of the METHOD, KEYFORMAT, and KEYFORMATVERSIONS attributes + MUST match any EXT-X-KEY with the same URI value. + + EXT-X-SESSION-KEY tags SHOULD be added if multiple Variant Streams or + Renditions use the same encryption keys and formats. An EXT-X- + SESSION-KEY tag is not associated with any particular Media Playlist. + + A Multivariant Playlist MUST NOT contain more than one EXT-X-SESSION- + KEY tag with the same METHOD, URI, IV, KEYFORMAT, and + KEYFORMATVERSIONS attribute values. + + The EXT-X-SESSION-KEY tag is optional. + */ + func test_EXT_X_SESSION_KEY() { + let tagData = "METHOD=SAMPLE-AES,URI=\"skd://key65\",KEYFORMAT=\"com.apple.streamingkeydelivery\"" + let tag = PantosTag.EXT_X_SESSION_KEY + // Splitting out the `validate` into constituent parts because when URI is empty it triggers more than one + // failure which trips up the `missingMandatoryKeys` method that expects just one failure. + validInput(tag: tag, tagData: tagData) + emptyInput(tag: tag, numberOfErrors: 2) + missingOptionalKeys(tag: tag, tagData: tagData, removed: [.ivector, .keyformat, .keyformatVersions]) + missingMandatoryKeys(tag: tag, tagData: tagData, removed: [.method]) + wrongType(tag: tag, tagData: tagData, badValues: []) + + let EXT_X_SESSION_KEY_withNoURIAndMETHODEqualToNONE = { + let tagData = "METHOD=NONE" + let tag = createHLSTag(tagDescriptor: PantosTag.EXT_X_SESSION_KEY, tagData: tagData) + guard let validator = PantosTag.validator(forTag: PantosTag.EXT_X_SESSION_KEY) else { + return XCTFail("No validator for EXT-X-SESSION-KEY") + } + guard let issues = validator.validate(tag: tag) else { + return XCTFail("Should have issues when validating EXT-X-SESSION-KEY when METHOD=NONE.") + } + XCTAssertEqual(2, issues.count, "Should have two issues") + for issue in issues { + if issue.description == IssueDescription.EXT_X_SESSION_KEYValidator.rawValue { + XCTAssertEqual(issue.severity, .error, "Should have error severity for EXT-X-SESSION-KEY issue.") + } else if issue.description == "EXT-X-SESSION-KEY mandatory value uri is missing." { + XCTAssertEqual(issue.severity, .error, "Should have error severity if URI is missing.") + } else { + XCTFail("Not expecting to have issue with description: \(issue.description)") + } + } + } + EXT_X_SESSION_KEY_withNoURIAndMETHODEqualToNONE() + } + + /* + The EXT-X-CONTENT-STEERING tag allows a server to provide a Content + Steering (Section 7) Manifest. It is OPTIONAL. It MUST NOT appear + more than once in a Multivariant Playlist. Its format is: + + #EXT-X-CONTENT-STEERING: + + The following attributes are defined: + + SERVER-URI + + The value is a quoted-string containing a URI to a Steering + Manifest (Section 7.1). It MAY contain an asset identifier if the + Steering Server requires it to produce the Steering Manifest. It + MAY use the "data" URI scheme to provide the manifest in-line in + the Multivariant Playlist; in that case, subsequent manifest + reloads MAY be redirected to a remote Steering Server using the + RELOAD-URI parameter (see Section 7.1). This attribute is + REQUIRED. + + PATHWAY-ID + + The value is a quoted-string that identifies the Pathway that MUST + be applied by any client that supports Content Steering (see + Section 7.4) until the initial Steering Manifest has been + obtained. Its value MUST be a legal Pathway ID according to + Section 7.1 that is specified by the PATHWAY-ID attribute of one + or more Variant Streams in the Multivariant Playlist. This + attribute is OPTIONAL. + */ + func test_EXT_X_CONTENT_STEERING() { + let tagData = "SERVER-URI=\"https://not.a.server/content-steering.json\",PATHWAY-ID=\"A\"" + validate(tag: PantosTag.EXT_X_CONTENT_STEERING, + tagData: tagData, + optional: [.pathwayId], + mandatory: [.serverUri], + badValues: []) + } + /* The EXT-X-BYTERANGE tag indicates that a media segment is a sub-range of the resource identified by its media URI. It applies only to the From 84b3a0b21baa05385fb342a7c6df6fb1bcfe25be Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Mon, 2 Sep 2024 11:26:09 -0400 Subject: [PATCH 02/18] Add attributes missing from EXT-X-MEDIA as of draft 15 --- .../PantosTag.swift | 6 +++++- .../PantosValue.swift | 14 +++++++++++++- .../GenericDictionaryTagValidatorTests.swift | 13 +++++++++---- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift index 38f6cd9..4ca697b 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift @@ -443,11 +443,15 @@ extension PantosTag: HLSTagDescriptor, Equatable { HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.language, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.assocLanguage, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.name, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.stableRenditionId, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.defaultMedia, optional: true, expectedType: Bool.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.autoselect, optional: true, expectedType: Bool.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.forced, optional: true, expectedType: Bool.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.instreamId, optional: true, expectedType: HLSInstreamId.self), - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.characteristics, optional: true, expectedType: String.self) + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.bitDepth, optional: true, expectedType: Int.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.sampleRate, optional: true, expectedType: Int.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.characteristics, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.channels, optional: true, expectedType: String.self) ]) case .EXT_X_I_FRAME_STREAM_INF: diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosValue.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosValue.swift index 350cb47..9a44494 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosValue.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosValue.swift @@ -114,7 +114,19 @@ public enum PantosValue: String { /// Found in `.EXT_X_MEDIA`. This attribute is REQUIRED if the TYPE attribute is CLOSED-CAPTIONS ("CC1", "CC2", "CC3", "CC4") case instreamId = "INSTREAM-ID" - + + /// Found in `.EXT_X_MEDIA`. Allows the URI to change between two reloads of the playlist. + case stableRenditionId = "STABLE-RENDITION-ID" + + /// Found in `.EXT_X_MEDIA`. Specifies the audio bit depth of the rendition. + case bitDepth = "BIT-DEPTH" + + /// Found in `.EXT_X_MEDIA`. Specifies the audio sample rate of the rendition. + case sampleRate = "SAMPLE-RATE" + + /// Found in `.EXT_X_MEDIA`. Provides information about audio channels, such as count, spatial audio coding, and other special channel usage instructions. + case channels = "CHANNELS" + /// Found in `.EXT_X_MEDIA`, `.EXT_X_KEY`, `.EXT_X_MAP`, `.EXT_X_I_FRAME_STREAM_INF` and `.EXT_X_SESSION_DATA`. The URI location of the media case uri = "URI" diff --git a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index f940aab..f2e8cf5 100644 --- a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift +++ b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift @@ -237,6 +237,7 @@ class GenericDictionaryTagValidatorTests: XCTestCase { .type, .groupId, .name, + .stableRenditionId, .language, .assocLanguage, .uri, @@ -244,15 +245,19 @@ class GenericDictionaryTagValidatorTests: XCTestCase { .autoselect, .forced, .instreamId, - .characteristics - ] + .bitDepth, + .sampleRate, + .characteristics, + .channels] let mandatory: [PantosValue] = [] let badValues: [PantosValue] = [.type, .defaultMedia, .autoselect, .forced, - .instreamId] - + .instreamId, + .bitDepth, + .sampleRate] + validate(tag: PantosTag.EXT_X_MEDIA, tagData: tagData, optional: optional, From 63aaabce836c40bece724a8202113a1a7dd85af1 Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Mon, 2 Sep 2024 12:11:14 -0400 Subject: [PATCH 03/18] Updated test comment with up-to-date docs for EXT-X-MEDIA --- .../GenericDictionaryTagValidatorTests.swift | 259 +++++++++++++----- 1 file changed, 188 insertions(+), 71 deletions(-) diff --git a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index f2e8cf5..ebb3c26 100644 --- a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift +++ b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift @@ -143,91 +143,208 @@ class GenericDictionaryTagValidatorTests: XCTestCase { The following attributes are defined: + TYPE + + The value is an enumerated-string; valid strings are AUDIO, VIDEO, + SUBTITLES, and CLOSED-CAPTIONS. This attribute is REQUIRED. + + Typically, closed-caption [CEA608] media is carried in the video + stream. Therefore, an EXT-X-MEDIA tag with TYPE of CLOSED- + CAPTIONS does not specify a Rendition; the closed-caption media is + present in the Media Segments of every video Rendition. + URI - + The value is a quoted-string containing a URI that identifies the - Playlist file. This attribute is optional; see Section 3.4.10.1. - - TYPE - - The value is enumerated-string; valid strings are AUDIO, VIDEO and - SUBTITLES. If the value is AUDIO, the Playlist described by the tag - MUST contain audio media. If the value is VIDEO, the Playlist MUST - contain video media. If the value is SUBTITLES, the Playlist MUST - contain subtitle media. - + Media Playlist file. This attribute is OPTIONAL; see + Section 4.4.6.2.1. If the TYPE is CLOSED-CAPTIONS, the URI + attribute MUST NOT be present. + GROUP-ID - - The value is a quoted-string identifying a mutually-exclusive group - of renditions. The presence of this attribute signals membership in - the group. See Section 3.4.9.1. - + + The value is a quoted-string that specifies the group to which the + Rendition belongs. See Section 4.4.6.1.1. This attribute is + REQUIRED. + LANGUAGE - - The value is a quoted-string containing an RFC 5646 [RFC5646] - language tag that identifies the primary language used in the - rendition. This attribute is optional. - + + The value is a quoted-string containing one of the standard Tags + for Identifying Languages [RFC5646], which identifies the primary + language used in the Rendition. This attribute is OPTIONAL. + ASSOC-LANGUAGE - - The value is a quoted-string containing an RFC 5646 [RFC5646] - language tag that identifies a language that is associated with the - rendition. An associated language is often used in a different role - than the language specified by the LANGUAGE attribute (e.g. written - vs. spoken, or as a fallback dialect). This attribute is OPTIONAL. - + + The value is a quoted-string containing a language tag [RFC5646] + that identifies a language that is associated with the Rendition. + An associated language is often used in a different role than the + language specified by the LANGUAGE attribute (e.g., written versus + spoken, or a fallback dialect). This attribute is OPTIONAL. + NAME - - The value is a quoted-string containing a human-readable description - of the rendition. If the LANGUAGE attribute is present then this - description SHOULD be in that language. - + + The value is a quoted-string containing a human-readable + description of the Rendition. If the LANGUAGE attribute is + present, then this description SHOULD be in that language. See + Appendix E for more information. This attribute is REQUIRED. + + STABLE-RENDITION-ID + + The value is a quoted-string which is a stable identifier for the + URI within the Multivariant Playlist. All characters in the + quoted-string MUST be from the following set: [a..z], [A..Z], + [0..9], '+', '/', '=', '.', '-', and '_'. This attribute is + OPTIONAL. + + The STABLE-RENDITION-ID allows the URI of a Rendition to change + between two distinct downloads of the Multivariant Playlist. IDs + are matched using a byte-for-byte comparison. + + All EXT-X-MEDIA tags in a Multivariant Playlist with the same URI + value SHOULD use the same STABLE-RENDITION-ID. + DEFAULT - - The value is an enumerated-string; valid strings are YES and NO. If - the value is YES, then the client SHOULD play this rendition of the - content in the absence of information from the user indicating a - different choice. This attribute is optional. Its absence indicates - an implicit value of NO. - + + The value is an enumerated-string; valid strings are YES and NO. + If the value is YES, then the client SHOULD play this Rendition of + the content in the absence of information from the user indicating + a different choice. This attribute is OPTIONAL. Its absence + indicates an implicit value of NO. + AUTOSELECT - + The value is an enumerated-string; valid strings are YES and NO. - This attribute is optional. If it is present, its value MUST be YES - if the value of the DEFAULT attribute is YES. If the value is YES, - then the client MAY choose to play this rendition in the absence of - explicit user preference because it matches the current playback - environment, such as chosen system language. - + This attribute is OPTIONAL. Its absence indicates an implicit + value of NO. If the value is YES, then the client MAY choose to + play this Rendition in the absence of explicit user preference + because it matches the current playback environment, such as + chosen system language. + + If the AUTOSELECT attribute is present, its value MUST be YES if + the value of the DEFAULT attribute is YES. + FORCED - + The value is an enumerated-string; valid strings are YES and NO. - This attribute is optional. Its absence indicates an implicit value - of NO. The FORCED attribute MUST NOT be present unless the TYPE is - SUBTITLES. - - A value of YES indicates that the rendition contains content which is - considered essential to play. When selecting a FORCED rendition, a - client should choose the one that best matches the current playback - environment (e.g. language). - - A value of NO indicates that the rendition contains content which is - intended to be played in response to explicit user request. - + This attribute is OPTIONAL. Its absence indicates an implicit + value of NO. The FORCED attribute MUST NOT be present unless the + TYPE is SUBTITLES. + + A value of YES indicates that the Rendition contains content that + is considered essential to play. When selecting a FORCED + Rendition, a client SHOULD choose the one that best matches the + current playback environment (e.g., language). + + A value of NO indicates that the Rendition contains content that + is intended to be played in response to explicit user request. + INSTREAM-ID - - The value is a quoted-string that specifies a rendition within the + + The value is a quoted-string that specifies a Rendition within the segments in the Media Playlist. This attribute is REQUIRED if the - TYPE attribute is CLOSED-CAPTIONS, in which case it MUST have one of - the values: "CC1", "CC2", "CC3", "CC4". For all other TYPE values, - the INSTREAM-ID SHOULD NOT be specified. - + TYPE attribute is CLOSED-CAPTIONS, in which case it MUST have one + of the values: "CC1", "CC2", "CC3", "CC4", or "SERVICEn" where n + MUST be an integer between 1 and 63 (e.g., "SERVICE9" or + "SERVICE42"). + + The values "CC1", "CC2", "CC3", and "CC4" identify a Line 21 Data + Services channel [CEA608]. The "SERVICE" values identify a + Digital Television Closed Captioning [CEA708] service block + number. + + For all other TYPE values, the INSTREAM-ID MUST NOT be specified. + + BIT-DEPTH + + The value is a non-negative decimal-integer specifying the audio + bit depth of the Rendition. This attribute is OPTIONAL. The + attribute allows players to identify Renditions that have a bit + depth appropriate to the available hardware. The BIT-DEPTH + attribute MUST NOT be present unless the TYPE is AUDIO. + + SAMPLE-RATE + + The value is a non-negative decimal-integer specifying the audio + sample rate of the Rendition. This attribute is OPTIONAL. The + attribute allows players to identify Renditions that may be played + without sample rate conversion. This is useful for lossless + encodings. The SAMPLE-RATE attribute MUST NOT be present unless + the TYPE is AUDIO. + CHARACTERISTICS - - The value is a quoted-string containing one or more Uniform Type - Identifiers [UTI] separated by comma (,) characters. This attribute - is optional. Each UTI indicates an individual characteristic of the - rendition. + + The value is a quoted-string containing one or more Media + Characteristic Tags (MCTs) separated by comma (,) characters. A + Media Characteristic Tag has the same format as the payload of a + media characteristic tag atom [MCT]. This attribute is OPTIONAL. + Each MCT indicates an individual characteristic of the Rendition. + + A SUBTITLES Rendition MAY include the following characteristics: + "public.accessibility.transcribes-spoken-dialog", + "public.accessibility.describes-music-and-sound", and + "public.easy-to-read" (which indicates that the subtitles have + been edited for ease of reading). + + An AUDIO Rendition MAY include the following characteristic: + "public.accessibility.describes-video". + + The CHARACTERISTICS attribute MAY include private MCTs. + + CHANNELS + + The value is a quoted-string that specifies an ordered, slash- + separated ("/") list of parameters. + + The CHANNELS attribute MUST NOT be present unless the TYPE is + AUDIO. The first parameter is a decimal-integer. Each succeeding + parameter is a comma-separated list of Identifiers. An Identifier + is a string containing characters from the set [A..Z], [0..9], and + '-'. + + The first parameter is a count of audio channels expressed as a + decimal-integer, indicating the maximum number of independent, + simultaneous audio channels present in any Media Segment in the + Rendition. For example, an AC-3 5.1 Rendition would have a + CHANNELS="6" attribute. + + The second parameter identifies the presence of spatial audio of + some kind, for example, object-based audio, in the Rendition. + This parameter is a comma-separated list of Audio Coding + Identifiers. This parameter is optional. The Audio Coding + Identifiers are codec-specific. A parameter value of consisting + solely of the dash character (0x2D) indicates that the audio is + only channel-based. + + The third parameter contains supplementary indications of special + channel usage that are necessary for informed selection and + processing. This parameter is a comma-separated list of Special + Usage Identifiers. This parameter is optional, however if it is + present the second parameter must be non-empty. The following + Special Usage Identifiers can be present in the third parameter: + + BINAURAL The audio is binaural (either recorded or synthesized). + It SHOULD NOT be dynamically spatialized. It is best suited + for delivery to headphones. + + IMMERSIVE The audio is pre-processed content that SHOULD NOT be + dynamically spatialized. It is suitable to deliver to either + headphones or speakers. + + DOWNMIX The audio is a downmix derivative of some other audio. + If desired, the downmix may be used as a subtitute for + alternative Renditions in the same group with compatible + attributes and a greater channel count. It MAY be dynamically + spatialized. + + Audio without a Special Usage Identifier MAY be dynamically + spatialized. + + No other CHANNELS parameters are currently defined. + + All audio EXT-X-MEDIA tags SHOULD have a CHANNELS attribute. If a + Multivariant Playlist contains two Renditions with the same NAME + encoded with the same codec but a different number of channels, + then the CHANNELS attribute is REQUIRED; otherwise, it is + OPTIONAL. */ func test_EXT_X_MEDIA() { From 28d401a5bcb8f2f8d15deb6a2427cd4253edd5d5 Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Mon, 2 Sep 2024 12:13:50 -0400 Subject: [PATCH 04/18] Add attributes missing from EXT-X-STREAM-INF as of draft 15 --- .../PantosTag.swift | 12 +- .../PantosValue.swift | 42 +- .../GenericDictionaryTagValidatorTests.swift | 396 +++++++++++++++--- 3 files changed, 381 insertions(+), 69 deletions(-) diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift index 4ca697b..1b4ad24 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift @@ -426,13 +426,23 @@ extension PantosTag: HLSTagDescriptor, Equatable { case .EXT_X_STREAM_INF: return GenericDictionaryTagValidator(tag: pantostag, dictionaryValueIdentifiers: [ HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.bandwidthBPS, optional: false, expectedType: Int.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.averageBandwidthBPS, optional: true, expectedType: Int.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.score, optional: true, expectedType: Double.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.programId, optional: true, expectedType: Int.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.codecs, optional: true, expectedType: HLSCodecArray.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.supplementalCodecs, optional: true, expectedType: HLSCodecArray.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.resolution, optional: true, expectedType: HLSResolution.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.frameRate, optional: true, expectedType: Double.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.stableVariantId, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.audioGroup, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoGroup, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.subtitlesGroup, optional: true, expectedType: String.self), - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.closedCaptionsGroup, optional: true, expectedType: HLSClosedCaptions.self) + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.closedCaptionsGroup, optional: true, expectedType: HLSClosedCaptions.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.pathwayId, optional: true, expectedType: String.self) ]) case .EXT_X_MEDIA: diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosValue.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosValue.swift index 9a44494..8f6e401 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosValue.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosValue.swift @@ -34,9 +34,15 @@ public enum PantosValue: String { /// Found in `.UnknownTag`. The data of the tag. case UnknownTag_Value = "UnknownTag_Value" - /// Found in `.EXT_X_STREAM_INF` and `.EXT_X_I_FRAME_STREAM_INF`. A bandwidth value in bits per second. + /// Found in `.EXT_X_STREAM_INF` and `.EXT_X_I_FRAME_STREAM_INF`. A peak bandwidth value in bits per second. case bandwidthBPS = "BANDWIDTH" - + + /// Found in `.EXT_X_STREAM_INF` and `.EXT_X_I_FRAME_STREAM_INF`. An average bandwidth value in bits per second. + case averageBandwidthBPS = "AVERAGE-BANDWIDTH" + + /// Found in `.EXT_X_STREAM_INF` and `.EXT_X_I_FRAME_STREAM_INF`. An abstract, relative measure of the playback quality-of-experience of the variant stream. + case score = "SCORE" + /// Found in `.EXT_X_STREAM_INF` and `.EXT_X_I_FRAME_STREAM_INF`. The program id of the stream. case programId = "PROGRAM-ID" @@ -48,10 +54,33 @@ public enum PantosValue: String { /// Found in `.EXT_X_STREAM_INF` and `.EXT_X_I_FRAME_STREAM_INF`. Comma delimited list of formats supported in the media file. case codecs = "CODECS" - + + /// Found in `.EXT_X_STREAM_INF` and `.EXT_X_I_FRAME_STREAM_INF`. Comma delimited list of formats supported in the enhancement layer in the media file. + case supplementalCodecs = "SUPPLEMENTAL-CODECS" + /// Found in `.EXT_X_STREAM_INF` and `.EXT_X_I_FRAME_STREAM_INF`. Horizonal by vertical pixel resolution of the media file, i.e. 1280x720 case resolution = "RESOLUTION" - + + /// Found in `.EXT_X_STREAM_INF` and `.EXT_X_I_FRAME_STREAM_INF`. Advisory information on the minimum HDCP level + /// required by the output protection level of the license that will be provided to decrypt this media content. + case hdcpLevel = "HDCP-LEVEL" + + /// Found in `.EXT_X_STREAM_INF` and `.EXT_X_I_FRAME_STREAM_INF`. Advisory information on the minimum robustness + /// level required by the output protection level of the license that will be provided to decrypt this media content. + case allowedCpc = "ALLOWED-CPC" + + /// Found in `.EXT_X_STREAM_INF` and `.EXT_X_I_FRAME_STREAM_INF`. Reference opto-electronic transfer characteristic function used in the encoding of the media file. + case videoRange = "VIDEO-RANGE" + + /// Found in `.EXT_X_STREAM_INF` and `.EXT_X_I_FRAME_STREAM_INF`. Indication of any specialized rendering needed to properly display the video content of the media file. + case reqVideoLayout = "REQ-VIDEO-LAYOUT" + + /// Found in `.EXT_X_STREAM_INF` and `.EXT_X_I_FRAME_STREAM_INF`. Allows the URI (defined in next Location line or the URI attribute) to change between two reloads of the playlist. + case stableVariantId = "STABLE-VARIANT-ID" + + /// Found in `.EXT_X_STREAM_INF`. Maximum frame rate for all video in the variant stream (rounded to 3 decimal places). + case frameRate = "FRAME-RATE" + /// Found in `.EXT_X_STREAM_INF`. Match a tag with a corresponding subtitles stream. case subtitlesGroup = "SUBTITLES" @@ -70,7 +99,10 @@ public enum PantosValue: String { /// Found in `.EXT_X_CONTENT_STEERING`. The URI location for the steering manifest. case serverUri = "SERVER-URI" - /// Found in `.EXT_X_CONTENT_STEERING`. The initial pathway to choose until the first steering manifest is obtained. + /// Found in `.EXT_X_STREAM_INF`, `.EXT_X_I_FRAME_STREAM_INF` and `.EXT_X_CONTENT_STEERING`. When found in + /// `.EXT_X_CONTENT_STEERING` it represents the initial pathway to choose until the first steering manifest is + /// obtained. When found in `.EXT_X_STREAM_INF` or `.EXT_X_I_FRAME_STREAM_INF` it represents the Content Steering + /// Pathway that the variant stream belongs to. case pathwayId = "PATHWAY-ID" /// Found in `.EXT_X_TARGETDURATION`. A target duration in seconds. diff --git a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index ebb3c26..9e1d1f2 100644 --- a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift +++ b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift @@ -401,96 +401,366 @@ class GenericDictionaryTagValidatorTests: XCTestCase { /* #EXT-X-STREAM-INF: - + The following attributes are defined: - + BANDWIDTH - - The value is a decimal-integer of bits per second. It MUST be an - upper bound of the overall bitrate of each media segment (calculated - to include container overhead) that appears or will appear in the - Playlist. - + + The value is a decimal-integer of bits per second. It represents + the peak segment bit rate of the Variant Stream. + + If all the Media Segments in a Variant Stream have already been + created, the BANDWIDTH value MUST be the largest sum of peak + segment bit rates that is produced by any playable combination of + Renditions. (For a Variant Stream with a single Media Playlist, + this is just the peak segment bit rate of that Media Playlist.) + An inaccurate value can cause playback stalls or prevent clients + from playing the variant. + + If the Multivariant Playlist is to be made available before all + Media Segments in the presentation have been encoded, the + BANDWIDTH value SHOULD be the BANDWIDTH value of a representative + period of similar content, encoded using the same settings. + Every EXT-X-STREAM-INF tag MUST include the BANDWIDTH attribute. - - PROGRAM-ID - - The value is a decimal-integer that uniquely identifies a particular - presentation within the scope of the Playlist file. - - A Playlist file MAY contain multiple EXT-X-STREAM-INF tags with the - same PROGRAM-ID to identify different encodings of the same - presentation. These variant playlists MAY contain additional EXT-X- - STREAM-INF tags. - + + AVERAGE-BANDWIDTH + + The value is a decimal-integer of bits per second. It represents + the average segment bit rate of the Variant Stream. + + If all the Media Segments in a Variant Stream have already been + created, the AVERAGE-BANDWIDTH value MUST be the largest sum of + average segment bit rates that is produced by any playable + combination of Renditions. (For a Variant Stream with a single + Media Playlist, this is just the average segment bit rate of that + Media Playlist.) An inaccurate value can cause playback stalls or + prevent clients from playing the variant. + + If the Multivariant Playlist is to be made available before all + Media Segments in the presentation have been encoded, the AVERAGE- + BANDWIDTH value SHOULD be the AVERAGE-BANDWIDTH value of a + representative period of similar content, encoded using the same + settings. + + The AVERAGE-BANDWIDTH attribute is OPTIONAL. + + SCORE + + The value is a positive decimal-floating-point number. It is an + abstract, relative measure of the playback quality-of-experience + of the Variant Stream. + + The value can be based on any metric or combination of metrics + that can be consistently applied to all Variant Streams. The + value SHOULD consider all media in the Variant Stream, including + video, audio and subtitles. A Variant Stream with a SCORE + attribute MUST be considered by the Playlist author to be more + desirable than any Variant Stream with a lower SCORE attribute in + the same Multivariant Playlist. + + The SCORE attribute is OPTIONAL, but if any Variant Stream + contains the SCORE attribute, then all Variant Streams in the + Multivariant Playlist SHOULD have a SCORE attribute. See + Section 6.3.1 for more information. + CODECS - + The value is a quoted-string containing a comma-separated list of formats, where each format specifies a media sample type that is - present in a media segment in the Playlist file. Valid format - identifiers are those in the ISO File Format Name Space defined by - RFC 6381 [RFC6381]. - + present in one or more Renditions specified by the Variant Stream. + Valid format identifiers are those in the ISO Base Media File + Format Name Space defined by "The 'Codecs' and 'Profiles' + Parameters for "Bucket" Media Types" [RFC6381]. + + For example, a stream containing AAC low complexity (AAC-LC) audio + and H.264 Main Profile Level 3.0 video would have a CODECS value + of "mp4a.40.2,avc1.4d401e". + + Note that if a Variant Stream specifies one or more Renditions + that include IMSC subtitles, the CODECS attribute MUST indicate + this with a format identifier such as "stpp.ttml.im1t". + Every EXT-X-STREAM-INF tag SHOULD include a CODECS attribute. - + + SUPPLEMENTAL-CODECS + + The SUPPLEMENTAL-CODECS attribute describes media samples with + both a backward-compatible base layer and a newer enhancement + layer. The base layers are specified in the CODECS attribute and + the enhancement layers are specified by the SUPPLEMENTAL-CODECS + attribute. + + The value is a quoted-string containing a comma-separated list of + elements, where each element specifies an enhancement layer media + sample type that is present in one or more Renditions specified by + the Variant Stream. + + Each element is a slash-separated list of fields. The first field + must be a valid CODECS format. If more than one field is present, + the remaining fields must be compatibility brands [MP4RA] that + pertain to that codec's bitstream. + + Each member of SUPPLEMENTAL-CODECS must have its base layer codec + declared in the CODECS attribute. + + For example, a stream containing Dolby Vision 8.4 content might + have a CODECS attribute including "hvc1.2.4.L153.b0", and a + SUPPLEMENTAL-CODECS attribute including "dvh1.08.07/db4h". + + The SUPPLEMENTAL-CODECS attribute is OPTIONAL. + RESOLUTION - - The value is a decimal-resolution describing the approximate encoded - horizontal and vertical resolution of video within the presentation. - + + The value is a decimal-resolution describing the optimal pixel + resolution at which to display all the video in the Variant + Stream. + + The RESOLUTION attribute is OPTIONAL but is recommended if the + Variant Stream includes video. + + FRAME-RATE + + The value is a decimal-floating-point describing the maximum frame + rate for all the video in the Variant Stream, rounded to three + decimal places. + + The FRAME-RATE attribute is OPTIONAL but is recommended if the + Variant Stream includes video. The FRAME-RATE attribute SHOULD be + included if any video in a Variant Stream exceeds 30 frames per + second. + + HDCP-LEVEL + + The value is an enumerated-string; valid strings are TYPE-0, TYPE- + 1, and NONE. This attribute is advisory. A value of TYPE-0 + indicates that the Variant Stream could fail to play unless the + output is protected by High-bandwidth Digital Content Protection + (HDCP) Type 0 [HDCP] or equivalent. A value of TYPE-1 indicates + that the Variant Stream could fail to play unless the output is + protected by HDCP Type 1 or equivalent. A value of NONE indicates + that the content does not require output copy protection. + + Encrypted Variant Streams with different HDCP levels SHOULD use + different media encryption keys. + + The HDCP-LEVEL attribute is OPTIONAL. It SHOULD be present if any + content in the Variant Stream will fail to play without HDCP. + Clients without output copy protection SHOULD NOT load a Variant + Stream with an HDCP-LEVEL attribute unless its value is NONE. + + ALLOWED-CPC + + The ALLOWED-CPC attribute allows a server to indicate that the + playback of a Variant Stream containing encrypted Media Segments + is to be restricted to devices that guarantee a certain level of + content protection robustness. Its value is a quoted-string + containing a comma-separated list of entries. Each entry consists + of a KEYFORMAT attribute value followed by a colon character (:) + followed by a sequence of Content Protection Configuration (CPC) + Labels separated by slash (/) characters. Each CPC Label is a + string containing characters from the set [A..Z], [0..9], and '-'. + + For example: ALLOWED-CPC="com.example.drm1:SMART-TV/PC, + com.example.drm2:HW" + + A CPC Label identifies a class of playback device that implements + the KEYFORMAT with a certain level of content protection + robustness. Each KEYFORMAT can define its own set of CPC Labels. + The "identity" KEYFORMAT does not define any labels. A KEYFORMAT + that defines CPC Labels SHOULD also specify its robustness + requirements in a secure manner in each key response. + + A client MAY play the Variant Stream if it implements one of the + listed KEYFORMAT schemes with content protection robustness that + matches one or more of the CPC Labels in the list. If it does not + match any of the CPC Labels then it SHOULD NOT attempt to play the + Variant Stream. + + The ALLOWED-CPC attribute is OPTIONAL. If it is not present or + does not contain a particular KEYFORMAT then all clients that + support that KEYFORMAT MAY play the Variant Stream. + + VIDEO-RANGE + + The value is an enumerated-string; valid strings are SDR, HLG and + PQ. + + The value MUST be SDR if the video in the Variant Stream is + encoded using one of the following reference opto-electronic + transfer characteristic functions specified by the + TransferCharacteristics code point: [CICP] 1, 6, 13, 14, 15. Note + that different TransferCharacteristics code points can use the + same transfer function. + + The value MUST be HLG if the video in the Variant Stream is + encoded using a reference opto-electronic transfer characteristic + function specified by the TransferCharacteristics code point 18, + or consists of such video mixed with video qualifying as SDR (see + above). + + The value MUST be PQ if the video in the Variant Stream is encoded + using a reference opto-electronic transfer characteristic function + specified by the TransferCharacteristics code point 16, or + consists of such video mixed with video qualifying as SDR or HLG + (see above). + + This attribute is OPTIONAL. Its absence implies a value of SDR. + Clients that do not recognize the attribute value SHOULD NOT + select the Variant Stream. + + REQ-VIDEO-LAYOUT + + The REQ-VIDEO-LAYOUT attribute indicates whether the video content + in the Variant Stream requires specialized rendering to be + properly displayed. Its value is a quoted-string containing a + comma-separated list of View Presentation Entries, where each + entry specifies the rendering for some portion of the Variant + Stream. + + Each View Presentation Entry consists of an unordered, slash- + separated list of specifiers. Each specifier controls one aspect + of the entry. That is, the specifiers are disjoint and the values + for a specifier are mutually exclusive. Each specifier can occur + at most once in an entry. The possible specifiers are given + below. + + All specifier values are enumerated-strings. The enumerated- + strings for a specifier will share a common-prefix. If the + specifier list contains an unrecognized enumerated-string then the + client MUST ignore the tag and the following URI line. + + The Video Channel Specifier is an enumerated-string that defines + the video channels; valid strings are CH-STEREO, and CH-MONO. A + value of CH-STEREO (stereoscopic) indicates that both left and + right eye images are present. A value of CH-MONO (monoscopic) + indicates that a single image is present. + + The REQ-VIDEO-LAYOUT attribute is optional. A REQ-VIDEO-LAYOUT + attribute MUST NOT be empty, and each View Presentation Entry MUST + NOT be empty. The attribute SHOULD be present if any content in + the Variant Stream will fail to display properly without + specialized rendering, otherwise playback errors can occur on some + clients. + + The client SHOULD assume that the order of entries reflects the + most common presentation in the content. For example, if the + content is predominantly stereoscopic, with some brief sections + that are monoscopic then the Multivariant Playlist SHOULD specify + REQ-VIDEO-LAYOUT="CH-STEREO,CH-MONO". On the other hand, if the + content is predominantly monoscopic then the Multivariant Playlist + SHOULD specify REQ-VIDEO-LAYOUT="CH-MONO,CH-STEREO"". + + By default a video variant is monoscopic, so an attribute + consisting entirely of REQ-VIDEO-LAYOUT="CH-MONO" is unnecessary + and SHOULD NOT be present. Eliminating it allows Multivariant + Playlists with a mix of monoscopic and stereoscopic variants to be + played by clients that do not handle the REQ-VIDEO-LAYOUT + attribute. + + STABLE-VARIANT-ID + + The value is a quoted-string which is a stable identifier for the + URI within the Multivariant Playlist. All characters in the + quoted-string MUST be from the following set: [a..z], [A..Z], + [0..9], '+', '/', '=', '.', '-', and '_'. This attribute is + OPTIONAL. + + The STABLE-VARIANT-ID allows the URI of the Variant Stream to + change between two distinct downloads of the Multivariant + Playlist. IDs are matched using a byte-for-byte comparison. + + All EXT-X-STREAM-INF tags in a Multivariant Playlist with the same + URI value SHOULD use the same STABLE-VARIANT-ID. + AUDIO - + The value is a quoted-string. It MUST match the value of the - GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Playlist - whose TYPE attribute is AUDIO. It indicates the set of audio - renditions that MAY be used when playing the presentation. See - Section 3.3.10.1. - + GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the + Multivariant Playlist whose TYPE attribute is AUDIO. It indicates + the set of audio Renditions that SHOULD be used when playing the + presentation. See Section 4.4.6.2.1. + + The AUDIO attribute is OPTIONAL. + VIDEO - + The value is a quoted-string. It MUST match the value of the - GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Playlist - whose TYPE attribute is VIDEO. It indicates the set of video - renditions that MAY be used when playing the presentation. See - Section 3.3.10.1. - + GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the + Multivariant Playlist whose TYPE attribute is VIDEO. It indicates + the set of video Renditions that SHOULD be used when playing the + presentation. See Section 4.4.6.2.1. + + The VIDEO attribute is OPTIONAL. + SUBTITLES - - The value is a quoted-string. It MUST match the value of the GROUP- - ID attribute of an EXT-X-MEDIA tag elsewhere in the Master Playlist - whose TYPE attribute is SUBTITLES. It indicates the set of subtitle - renditions that MAY be used when playing the presentation. See Section 3.4.10.1. - + + The value is a quoted-string. It MUST match the value of the + GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the + Multivariant Playlist whose TYPE attribute is SUBTITLES. It + indicates the set of subtitle Renditions that can be used when + playing the presentation. See Section 4.4.6.2.1. + + The SUBTITLES attribute is OPTIONAL. + CLOSED-CAPTIONS - - The value can be either a quoted-string or an enumerated-string with - the value NONE. If the value is a quoted-string, it MUST match the - value of the GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in - the Playlist whose TYPE attribute is CLOSED-CAPTIONS, and indicates - the set of closed-caption renditions that may be used when playlist - the presentation. - - If the value is the enumerated-string value NONE, all EXT-X-STREAM- - INF tags MUST have this attribute with a value of NONE. This - indicates that there are no closed captions in any variant stream in - the Master Playlist + + The value can be either a quoted-string or an enumerated-string + with the value NONE. If the value is a quoted-string, it MUST + match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag + elsewhere in the Playlist whose TYPE attribute is CLOSED-CAPTIONS, + and it indicates the set of closed-caption Renditions that can be + used when playing the presentation. See Section 4.4.6.2.1. + + If the value is the enumerated-string value NONE, all EXT-X- + STREAM-INF tags MUST have this attribute with a value of NONE, + indicating that there are no closed captions in any Variant Stream + in the Multivariant Playlist. Having closed captions in one + Variant Stream but not another can trigger playback + inconsistencies. + + The CLOSED-CAPTIONS attribute is OPTIONAL. + + PATHWAY-ID + + The value is a quoted-string. It indicates that the Variant + Stream belongs to the identified Content Steering (Section 7) + Pathway. This attribute is OPTIONAL. Its absence indicates that + the Variant Stream belongs to the default Pathway ".", so every + Variant Stream can be associated with a named Pathway. + + A Content Producer SHOULD provide all Rendition Groups on all + Pathways. A Variant Stream belonging to a particular Pathway + SHOULD use Rendition Group(s) on that Pathway. */ func test_EXT_X_STREAM_INF() { let tagData = "PROGRAM-ID=1,BANDWIDTH=2855600,CODECS=\"avc1.4d001f,mp4a.40.2\",RESOLUTION=960x540" - let optional: [PantosValue] = [.audioGroup, + let optional: [PantosValue] = [.averageBandwidthBPS, + .score, + .audioGroup, .programId, .resolution, .videoGroup, .subtitlesGroup, .closedCaptionsGroup, - .codecs] + .codecs, + .supplementalCodecs, + .hdcpLevel, + .allowedCpc, + .videoRange, + .reqVideoLayout, + .stableVariantId, + .frameRate] let mandatory: [PantosValue] = [.bandwidthBPS] let badValues: [PantosValue] = [.bandwidthBPS, + .averageBandwidthBPS, + .score, .programId, - .resolution] - + .resolution, + .frameRate] + validate(tag: PantosTag.EXT_X_STREAM_INF, tagData: tagData, optional: optional, From 064b48d2f2c041970c47a09a9769132b396574d7 Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Mon, 2 Sep 2024 12:20:48 -0400 Subject: [PATCH 05/18] Add attributes missing from EXT-X-I-FRAME-STREAM-INF as of draft 15 --- .../PantosTag.swift | 13 +++- .../GenericDictionaryTagValidatorTests.swift | 64 +++++++++++-------- 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift index 1b4ad24..05300de 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift @@ -466,12 +466,21 @@ extension PantosTag: HLSTagDescriptor, Equatable { case .EXT_X_I_FRAME_STREAM_INF: return GenericDictionaryTagValidator(tag: pantostag, dictionaryValueIdentifiers: [ + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.uri, optional: false, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.bandwidthBPS, optional: false, expectedType: Int.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.averageBandwidthBPS, optional: true, expectedType: Int.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.score, optional: true, expectedType: Double.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.programId, optional: true, expectedType: Int.self), - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.codecs, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.codecs, optional: true, expectedType: HLSCodecArray.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.supplementalCodecs, optional: true, expectedType: HLSCodecArray.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.resolution, optional: true, expectedType: HLSResolution.self), - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.uri, optional: false, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.stableVariantId, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoGroup, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.pathwayId, optional: true, expectedType: String.self), ]) case .EXT_X_SESSION_DATA: diff --git a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index 9e1d1f2..1291880 100644 --- a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift +++ b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift @@ -857,50 +857,58 @@ class GenericDictionaryTagValidatorTests: XCTestCase { badValues: badValues) } /* - The EXT-X-I-FRAME-STREAM-INF tag identifies a Playlist file + The EXT-X-I-FRAME-STREAM-INF tag identifies a Media Playlist file containing the I-frames of a multimedia presentation. It stands - alone, in that it does not apply to a particular URI in the Playlist. - Its format is: - + alone, in that it does not apply to a particular URI in the + Multivariant Playlist. Its format is: + #EXT-X-I-FRAME-STREAM-INF: - - All attributes defined for the EXT-X-STREAM-INF tag (Section 3.3.10) + + All attributes defined for the EXT-X-STREAM-INF tag (Section 4.4.6.2) are also defined for the EXT-X-I-FRAME-STREAM-INF tag, except for the - AUDIO attribute. In addition, the following attribute is defined: - - URI - - The value is a quoted-string containing a URI that identifies the - I-frame Playlist file. - + FRAME-RATE, AUDIO, SUBTITLES, and CLOSED-CAPTIONS attributes. In + addition, the following attribute is defined: + + URI + + The value is a quoted-string containing a URI that identifies the + I-frame Media Playlist file. That Playlist file MUST contain an + EXT-X-I-FRAMES-ONLY tag. + Every EXT-X-I-FRAME-STREAM-INF tag MUST include a BANDWIDTH attribute and a URI attribute. - - The provisions in Section 3.3.10.1 also apply to EXT-X-I-FRAME- + + The provisions in Section 4.4.6.2.1 also apply to EXT-X-I-FRAME- STREAM-INF tags with a VIDEO attribute. - - A Playlist that specifies alternative VIDEO renditions and I-frame - Playlists SHOULD include an alternative I-frame VIDEO rendition for - each regular VIDEO rendition, with the same NAME and LANGUAGE - attributes. - - The EXT-X-I-FRAME-STREAM-INF tag appeared in version 4 of the - protocol. Clients supporting earlier protocol versions MUST ignore - it. + + A Multivariant Playlist that specifies alternative VIDEO Renditions + and I-frame Playlists SHOULD include an alternative I-frame VIDEO + Rendition for each regular VIDEO Rendition, with the same NAME and + LANGUAGE attributes. */ func test_EXT_I_FRAME_STREAM_INF() { let tagData = "BANDWIDTH=328400,PROGRAM-ID=1,CODECS=\"avc1.4d401f\",RESOLUTION=320x180,URI=\"Simpsons_505_HD_VOD_STUNT_movie_LVLH05/format-hls-track-iframe-bandwidth-328400-repid-328400.m3u8\"" - let optional: [PantosValue] = [.programId, - .codecs, + let optional: [PantosValue] = [.averageBandwidthBPS, + .score, + .programId, .resolution, - .videoGroup] + .videoGroup, + .codecs, + .supplementalCodecs, + .hdcpLevel, + .allowedCpc, + .videoRange, + .reqVideoLayout, + .stableVariantId] let mandatory: [PantosValue] = [.bandwidthBPS, .uri] let badValues: [PantosValue] = [.bandwidthBPS, + .averageBandwidthBPS, + .score, .programId, .resolution] - + validate(tag: PantosTag.EXT_X_I_FRAME_STREAM_INF, tagData: tagData, optional: optional, From 9390aed9c10adb0350109ca111e9a700bc6a26be Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Mon, 2 Sep 2024 12:25:19 -0400 Subject: [PATCH 06/18] Updated encryption method enum with new type --- mambaSharedFramework/HLSValueTypes.swift | 1 + .../GenericDictionaryTagValidatorTests.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mambaSharedFramework/HLSValueTypes.swift b/mambaSharedFramework/HLSValueTypes.swift index d7c09e7..204e6ba 100644 --- a/mambaSharedFramework/HLSValueTypes.swift +++ b/mambaSharedFramework/HLSValueTypes.swift @@ -111,6 +111,7 @@ public struct HLSEncryptionMethodType: Equatable, FailableStringLiteralConvertib case None = "NONE" case AES128 = "AES-128" case SampleAES = "SAMPLE-AES" + case SampleAESCTR = "SAMPLE-AES-CTR" } public init?(string: String) { self.init(encryption: string) diff --git a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index 1291880..15f66bf 100644 --- a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift +++ b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift @@ -1057,7 +1057,7 @@ class GenericDictionaryTagValidatorTests: XCTestCase { emptyInput(tag: tag, numberOfErrors: 2) missingOptionalKeys(tag: tag, tagData: tagData, removed: [.ivector, .keyformat, .keyformatVersions]) missingMandatoryKeys(tag: tag, tagData: tagData, removed: [.method]) - wrongType(tag: tag, tagData: tagData, badValues: []) + wrongType(tag: tag, tagData: tagData, badValues: [.method]) let EXT_X_SESSION_KEY_withNoURIAndMETHODEqualToNONE = { let tagData = "METHOD=NONE" From a46b9e446288734d88d3eea34dde8196a8510e04 Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Mon, 2 Sep 2024 13:24:49 -0400 Subject: [PATCH 07/18] Added parsing for CHANNELS attribute --- mamba.xcodeproj/project.pbxproj | 8 ++ mambaSharedFramework/HLSValueTypes.swift | 103 ++++++++++++++ .../PantosTag.swift | 2 +- .../GenericDictionaryTagValidatorTests.swift | 3 +- .../Value Types/HLSChannelsTests.swift | 134 ++++++++++++++++++ 5 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 mambaTests/Util Tests/Value Types/HLSChannelsTests.swift diff --git a/mamba.xcodeproj/project.pbxproj b/mamba.xcodeproj/project.pbxproj index 1f68096..f9973dc 100644 --- a/mamba.xcodeproj/project.pbxproj +++ b/mamba.xcodeproj/project.pbxproj @@ -17,6 +17,9 @@ 144758352C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758342C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift */; }; 144758362C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758342C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift */; }; 144758372C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758342C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift */; }; + 144758392C8620C000D12CCD /* HLSChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758382C8620C000D12CCD /* HLSChannelsTests.swift */; }; + 1447583A2C8620C000D12CCD /* HLSChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758382C8620C000D12CCD /* HLSChannelsTests.swift */; }; + 1447583B2C8620C000D12CCD /* HLSChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758382C8620C000D12CCD /* HLSChannelsTests.swift */; }; 1D28F3451EAA9E500010320B /* hls_ad_master_playlist.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 1D28F3401EAA9E500010320B /* hls_ad_master_playlist.m3u8 */; }; 1D28F3461EAA9E500010320B /* hls_ad_variant_playlist.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 1D28F3411EAA9E500010320B /* hls_ad_variant_playlist.m3u8 */; }; 1D28F3471EAA9E500010320B /* hls_master_playlist_sap.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 1D28F3421EAA9E500010320B /* hls_master_playlist_sap.m3u8 */; }; @@ -626,6 +629,7 @@ 1447582C2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXT_X_SESSION_KEYValidator.swift; sourceTree = ""; }; 144758302C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXT_X_SESSION_DATATagValidator.swift; sourceTree = ""; }; 144758342C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXT_X_SESSION_DATAPlaylistValidator.swift; sourceTree = ""; }; + 144758382C8620C000D12CCD /* HLSChannelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HLSChannelsTests.swift; sourceTree = ""; }; 1D28F3401EAA9E500010320B /* hls_ad_master_playlist.m3u8 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = hls_ad_master_playlist.m3u8; sourceTree = ""; }; 1D28F3411EAA9E500010320B /* hls_ad_variant_playlist.m3u8 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = hls_ad_variant_playlist.m3u8; sourceTree = ""; }; 1D28F3421EAA9E500010320B /* hls_master_playlist_sap.m3u8 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = hls_master_playlist_sap.m3u8; sourceTree = ""; }; @@ -1325,6 +1329,7 @@ EC9BCAA21D749D8B0032BEBE /* Value Types */ = { isa = PBXGroup; children = ( + 144758382C8620C000D12CCD /* HLSChannelsTests.swift */, EC7492AF1DD29F8900AF4E20 /* HLSCodecArrayTests.swift */, EC7492B01DD29F8900AF4E20 /* HLSMediaTypeTests.swift */, EC7492B11DD29F8900AF4E20 /* HLSPlaylistTypeTests.swift */, @@ -1926,6 +1931,7 @@ 43DE4EFF1E564E1500EEE800 /* HLSMediaSpanTests.swift in Sources */, ECE36DE41F2A9F10005E5DA7 /* HLSPlaylistTimelineAndSequencingTests.swift in Sources */, 01CD2E7A1DE4D46F002510E7 /* EXT_X_MAPTagParserTests.swift in Sources */, + 144758392C8620C000D12CCD /* HLSChannelsTests.swift in Sources */, EC7492481DD29E7300AF4E20 /* HLSValidatorTests.swift in Sources */, EC7492AB1DD29F7000AF4E20 /* OrderedDictionaryTests.swift in Sources */, EC74923E1DD29E7300AF4E20 /* HLSParser_Super8DemuxedTests.swift in Sources */, @@ -2094,6 +2100,7 @@ 43DE4F001E564E1500EEE800 /* HLSMediaSpanTests.swift in Sources */, EC7492B41DD29F8900AF4E20 /* HLSCodecArrayTests.swift in Sources */, ECE36DE51F2A9F10005E5DA7 /* HLSPlaylistTimelineAndSequencingTests.swift in Sources */, + 1447583A2C8620C000D12CCD /* HLSChannelsTests.swift in Sources */, EC7492491DD29E7300AF4E20 /* HLSValidatorTests.swift in Sources */, EC7492AC1DD29F7000AF4E20 /* OrderedDictionaryTests.swift in Sources */, EC74923F1DD29E7300AF4E20 /* HLSParser_Super8DemuxedTests.swift in Sources */, @@ -2262,6 +2269,7 @@ ECE253FD209A50B500D388CE /* ThirdPartyTagListSupportTests.swift in Sources */, ECE25408209A50B500D388CE /* HLSResolutionTests.swift in Sources */, ECE253F6209A50B500D388CE /* GenericSingleValueTagParserTests.swift in Sources */, + 1447583B2C8620C000D12CCD /* HLSChannelsTests.swift in Sources */, ECE253FA209A50B500D388CE /* GenericSingleTagValidatorTests.swift in Sources */, ECE253DF209A509900D388CE /* HLSParserTest.swift in Sources */, ECE253F1209A50B500D388CE /* EXT_X_KEYTagParserTests.swift in Sources */, diff --git a/mambaSharedFramework/HLSValueTypes.swift b/mambaSharedFramework/HLSValueTypes.swift index 204e6ba..ade5351 100644 --- a/mambaSharedFramework/HLSValueTypes.swift +++ b/mambaSharedFramework/HLSValueTypes.swift @@ -190,6 +190,109 @@ public struct HLSClosedCaptions: FailableStringLiteralConvertible { } } +/// Represents CHANNELS +public struct HLSChannels: Equatable, FailableStringLiteralConvertible { + /// A count of audio channels, indicating the maximum number of independent, simultaneous audio channels present in + /// any Media Segment in the Rendition. + /// + /// For example, an AC-3 5.1 Rendition would have a CHANNELS="6" attribute. + public let count: Int + /// Identifies the presence of spatial audio of some kind, for example, object-based audio, in the Rendition. The + /// Audio Coding Identifiers are codec-specific. + public let spatialAudioCodingIdentifiers: [String] + /// Supplementary indications of special channel usage that are necessary for informed selection and processing. + /// This parameter is an array of Special Usage Identifiers. + public let specialUsageIdentifiers: [SpecialUsageIdentifier] + + public enum SpecialUsageIdentifier: String { + /// The audio is binaural (either recorded or synthesized). It SHOULD NOT be dynamically spatialized. It is best + /// suited for delivery to headphones. + case binaural = "BINAURAL" + /// The audio is pre-processed content that SHOULD NOT be dynamically spatialized. It is suitable to deliver to + /// either headphones or speakers. + case immersive = "IMMERSIVE" + /// The audio is a downmix derivative of some other audio. If desired, the downmix may be used as a subtitute + /// for alternative Renditions in the same group with compatible attributes and a greater channel count. It MAY + /// be dynamically spatialized. + case downmix = "DOWNMIX" + + /// Allows `init` without having to allocate a new `String` object. + init?(str: Substring) { + switch str { + case "BINAURAL": self = .binaural + case "IMMERSIVE": self = .immersive + case "DOWNMIX": self = .downmix + default: return nil + } + } + } + + public init?(string: String) { + var count: Int? + var spatialAudioCodingIdentifiers: [String]? + var specialUsageIdentifiers: [SpecialUsageIdentifier]? + let enumeratedSplit = string.split(separator: "/").enumerated() + for (index, str) in enumeratedSplit { + switch index { + case 0: count = Self.parseChannelCount(str: str) + case 1: spatialAudioCodingIdentifiers = Self.parseSpatialAudioCodingIdentifiers(str: str) + case 2: + guard let ids = Self.parseSpecialUsageIdentifiers(str: str) else { + // In the case that we don't recognize one of the special usage identifiers, leading to nil being + // parsed out, I believe it is better to fail the entire parsing, as otherwise we could mislead the + // user of the library into thinking that there are less special usage identifiers than there + // actually are in the CHANNELS attribtue. + return nil + } + specialUsageIdentifiers = ids + default: break // In the future there may be more parameters defined. + } + } + // Count is required to have been parsed. + guard let count else { + return nil + } + self.count = count + self.spatialAudioCodingIdentifiers = spatialAudioCodingIdentifiers ?? [] + self.specialUsageIdentifiers = specialUsageIdentifiers ?? [] + } + + public init( + count: Int, + spatialAudioCodingIdentifiers: [String], + specialUsageIdentifiers: [SpecialUsageIdentifier] + ) { + self.count = count + self.spatialAudioCodingIdentifiers = spatialAudioCodingIdentifiers + self.specialUsageIdentifiers = specialUsageIdentifiers + } + + private static func parseChannelCount(str: Substring) -> Int? { + Int(string: String(str)) + } + + private static func parseSpatialAudioCodingIdentifiers(str: Substring) -> [String] { + let split = str.split(separator: ",") + var identifiers = [String]() + for id in split where id != "-" { + identifiers.append(String(id)) + } + return identifiers + } + + private static func parseSpecialUsageIdentifiers(str: Substring) -> [SpecialUsageIdentifier]? { + let split = str.split(separator: ",") + var identifiers = [SpecialUsageIdentifier]() + for id in split { + guard let specialUsageId = SpecialUsageIdentifier(str: id) else { + return nil + } + identifiers.append(specialUsageId) + } + return identifiers + } +} + /// Represents a RFC6381 codec /// /// We are currently not parsing these values further diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift index 05300de..efa5473 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift @@ -461,7 +461,7 @@ extension PantosTag: HLSTagDescriptor, Equatable { HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.bitDepth, optional: true, expectedType: Int.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.sampleRate, optional: true, expectedType: Int.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.characteristics, optional: true, expectedType: String.self), - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.channels, optional: true, expectedType: String.self) + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.channels, optional: true, expectedType: HLSChannels.self) ]) case .EXT_X_I_FRAME_STREAM_INF: diff --git a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index 15f66bf..0a97783 100644 --- a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift +++ b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift @@ -373,7 +373,8 @@ class GenericDictionaryTagValidatorTests: XCTestCase { .forced, .instreamId, .bitDepth, - .sampleRate] + .sampleRate, + .channels] validate(tag: PantosTag.EXT_X_MEDIA, tagData: tagData, diff --git a/mambaTests/Util Tests/Value Types/HLSChannelsTests.swift b/mambaTests/Util Tests/Value Types/HLSChannelsTests.swift new file mode 100644 index 0000000..9807edf --- /dev/null +++ b/mambaTests/Util Tests/Value Types/HLSChannelsTests.swift @@ -0,0 +1,134 @@ +// +// HLSChannelsTests.swift +// mamba +// +// Created by Robert Galluccio on 9/2/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 +import XCTest +@testable import mamba + +class HLSChannelsTests: XCTestCase { + let empty = "" + let invalidCount = "ONE" + let sixChannel = "6" + let twelveChannelJoc = "12/JOC" + let twelveChannelJocAndUnknownSpatialCoding = "12/JOC,SPECIAL" + let sixChannelWithEmptySpatialIdentifier = "6/-" + let twelveChannelUnknownSpatialWithDashInName = "12/VERY-SPATIAL" + let sixChannelNoSpatialWithDownmix = "6/-/DOWNMIX" + let sixChannelNoSpatialWithBinauralAndImmersive = "6/-/BINAURAL,IMMERSIVE" + let twelveChannelJocAndImmersive = "12/JOC/IMMERSIVE" + let sixChannelUnknownSpecialUsageIdentifier = "6/-/NEW-IDENTIFIER" + + func test_empty() { + let actualChannels = HLSChannels(string: empty) + XCTAssertNil(actualChannels) + } + + func test_invalidCount() { + let actualChannels = HLSChannels(string: invalidCount) + XCTAssertNil(actualChannels) + } + + func test_sixChannel() { + let actualChannels = HLSChannels(string: sixChannel) + let expectedChannels = HLSChannels( + count: 6, + spatialAudioCodingIdentifiers: [], + specialUsageIdentifiers: [] + ) + XCTAssertEqual(expectedChannels, actualChannels) + } + + func test_twelveChannelJoc() { + let actualChannels = HLSChannels(string: twelveChannelJoc) + let expectedChannels = HLSChannels( + count: 12, + spatialAudioCodingIdentifiers: ["JOC"], + specialUsageIdentifiers: [] + ) + XCTAssertEqual(expectedChannels, actualChannels) + } + + func test_twelveChannelJocAndUnknownSpatialCoding() { + let actualChannels = HLSChannels(string: twelveChannelJocAndUnknownSpatialCoding) + let expectedChannels = HLSChannels( + count: 12, + spatialAudioCodingIdentifiers: ["JOC", "SPECIAL"], + specialUsageIdentifiers: [] + ) + XCTAssertEqual(expectedChannels, actualChannels) + } + + func test_sixChannelWithEmptySpatialIdentifier() { + let actualChannels = HLSChannels(string: sixChannelWithEmptySpatialIdentifier) + let expectedChannels = HLSChannels( + count: 6, + spatialAudioCodingIdentifiers: [], + specialUsageIdentifiers: [] + ) + XCTAssertEqual(expectedChannels, actualChannels) + } + + func test_twelveChannelUnknownSpatialWithDashInName() { + let actualChannels = HLSChannels(string: twelveChannelUnknownSpatialWithDashInName) + let expectedChannels = HLSChannels( + count: 12, + spatialAudioCodingIdentifiers: ["VERY-SPATIAL"], + specialUsageIdentifiers: [] + ) + XCTAssertEqual(expectedChannels, actualChannels) + } + + func test_sixChannelNoSpatialWithDownmix() { + let actualChannels = HLSChannels(string: sixChannelNoSpatialWithDownmix) + let expectedChannels = HLSChannels( + count: 6, + spatialAudioCodingIdentifiers: [], + specialUsageIdentifiers: [.downmix] + ) + XCTAssertEqual(expectedChannels, actualChannels) + } + + func test_sixChannelNoSpatialWithBinauralAndImmersive() { + let actualChannels = HLSChannels(string: sixChannelNoSpatialWithBinauralAndImmersive) + let expectedChannels = HLSChannels( + count: 6, + spatialAudioCodingIdentifiers: [], + specialUsageIdentifiers: [.binaural, .immersive] + ) + XCTAssertEqual(expectedChannels, actualChannels) + } + + func test_twelveChannelJocAndImmersive() { + let actualChannels = HLSChannels(string: twelveChannelJocAndImmersive) + let expectedChannels = HLSChannels( + count: 12, + spatialAudioCodingIdentifiers: ["JOC"], + specialUsageIdentifiers: [.immersive] + ) + XCTAssertEqual(expectedChannels, actualChannels) + } + + // In the case that we don't recognize the special usage identifier, I think it is better to fail parsing the entire + // CHANNELS attribute, as otherwise we risk misleading the user of the library into thinking that the special usage + // is less than it actually is. + func test_sixChannelUnknownSpecialUsageIdentifier() { + let actualChannels = HLSChannels(string: sixChannelUnknownSpecialUsageIdentifier) + XCTAssertNil(actualChannels) + } +} From 24418043a47cc8240525c3fc15061dc208e63ff0 Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Mon, 2 Sep 2024 19:52:31 -0400 Subject: [PATCH 08/18] Added parsing for HDCP-LEVEL --- mambaSharedFramework/HLSValueTypes.swift | 29 ++++++++++++++++++- .../PantosTag.swift | 4 +-- .../GenericDictionaryTagValidatorTests.swift | 6 ++-- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/mambaSharedFramework/HLSValueTypes.swift b/mambaSharedFramework/HLSValueTypes.swift index ade5351..0d14794 100644 --- a/mambaSharedFramework/HLSValueTypes.swift +++ b/mambaSharedFramework/HLSValueTypes.swift @@ -104,7 +104,7 @@ public func ==(lhs: HLSMediaType, rhs: HLSMediaType) -> Bool { /// Represents an encryption method /// -/// Can be initialized with a string "NONE" or "AES-128" or "SAMPLE-AES" for a valid value +/// Can be initialized with a string "NONE" or "AES-128" or "SAMPLE-AES" or "SAMPLE-AES-CTR" for a valid value public struct HLSEncryptionMethodType: Equatable, FailableStringLiteralConvertible { public let type: EncryptionMethod public enum EncryptionMethod: String { @@ -131,6 +131,33 @@ public func ==(lhs: HLSEncryptionMethodType, rhs: HLSEncryptionMethodType) -> Bo return lhs.type == rhs.type } +/// Represents a minimum required HDCP level needed to play content. +public struct HLSHDCPLevel: Equatable, FailableStringLiteralConvertible { + public let type: HDCPLevel + public enum HDCPLevel: String { + /// Indicates that the content does not require output copy protections. + case none = "NONE" + /// Indicates that the Variant Stream could fail to play unless the output is protected by High-bandwidth + /// Digital Content Protection (HDCP) Type 0 or equivalent. + case type0 = "TYPE-0" + /// Indicates that the Variant Stream could fail to play unless the output is protected by HDCP Type 1 or + /// equivalent. + case type1 = "TYPE-1" + } + public init?(string: String) { + self.init(hdcpLevel: string) + } + public init?(hdcpLevel: String) { + guard let type = HDCPLevel(rawValue: hdcpLevel) else { + return nil + } + self.type = type + } + public init(hdcpLevel: HDCPLevel) { + self.type = hdcpLevel + } +} + /// Represents a playlist type /// /// Can be initialized with a string "EVENT" or "VOD" for a valid value diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift index efa5473..2498a3d 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift @@ -433,7 +433,7 @@ extension PantosTag: HLSTagDescriptor, Equatable { HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.supplementalCodecs, optional: true, expectedType: HLSCodecArray.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.resolution, optional: true, expectedType: HLSResolution.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.frameRate, optional: true, expectedType: Double.self), - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: HLSHDCPLevel.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: String.self), @@ -474,7 +474,7 @@ extension PantosTag: HLSTagDescriptor, Equatable { HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.codecs, optional: true, expectedType: HLSCodecArray.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.supplementalCodecs, optional: true, expectedType: HLSCodecArray.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.resolution, optional: true, expectedType: HLSResolution.self), - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: HLSHDCPLevel.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: String.self), diff --git a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index 0a97783..cd9c474 100644 --- a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift +++ b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift @@ -760,7 +760,8 @@ class GenericDictionaryTagValidatorTests: XCTestCase { .score, .programId, .resolution, - .frameRate] + .frameRate, + .hdcpLevel] validate(tag: PantosTag.EXT_X_STREAM_INF, tagData: tagData, @@ -908,7 +909,8 @@ class GenericDictionaryTagValidatorTests: XCTestCase { .averageBandwidthBPS, .score, .programId, - .resolution] + .resolution, + .hdcpLevel] validate(tag: PantosTag.EXT_X_I_FRAME_STREAM_INF, tagData: tagData, From 492bc60a32208f6e94a070bf1cbad985f2065947 Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Mon, 2 Sep 2024 20:06:26 -0400 Subject: [PATCH 09/18] Added parsing for VIDEO-RANGE --- mambaSharedFramework/HLSValueTypes.swift | 38 +++++++++++++++++++ .../PantosTag.swift | 4 +- .../GenericDictionaryTagValidatorTests.swift | 6 ++- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/mambaSharedFramework/HLSValueTypes.swift b/mambaSharedFramework/HLSValueTypes.swift index 0d14794..3afb721 100644 --- a/mambaSharedFramework/HLSValueTypes.swift +++ b/mambaSharedFramework/HLSValueTypes.swift @@ -158,6 +158,44 @@ public struct HLSHDCPLevel: Equatable, FailableStringLiteralConvertible { } } +/// Represents the dynamic range of the video. +/// +/// This is represented by an enumeration where each case covers a group of similar opto-electronic transfer +/// characteristic functions that could have been used to encode the media file. +/// +/// For example, `SDR` covers TransferCharacteristics code points 1, 6, 13, 14 and 15. More information on what each +/// code point represents can be found in _"Information technology - MPEG systems technologies - Part 8: Coding-_ +/// _independent code points" ISO/IEC International Standard 23001-8, 2016_ [CICP]. +public struct HLSVideoRange: Equatable, FailableStringLiteralConvertible { + public let type: VideoRange + public enum VideoRange: String { + /// The value MUST be SDR if the video in the Variant Stream is encoded using one of the following reference + /// opto-electronic transfer characteristic functions specified by the TransferCharacteristics code point: 1, 6, + /// 13, 14, 15. Note that different TransferCharacteristics code points can use the same transfer function. + case sdr = "SDR" + /// The value MUST be HLG if the video in the Variant Stream is encoded using a reference opto-electronic + /// transfer characteristic function specified by the TransferCharacteristics code point 18, or consists of such + /// video mixed with video qualifying as SDR. + case hlg = "HLG" + /// The value MUST be PQ if the video in the Variant Stream is encoded using a reference opto-electronic + /// transfer characteristic function specified by the TransferCharacteristics code point 16, or consists of such + /// video mixed with video qualifying as SDR or HLG. + case pq = "PQ" + } + public init?(string: String) { + self.init(videoRange: string) + } + public init?(videoRange: String) { + guard let type = VideoRange(rawValue: videoRange) else { + return nil + } + self.type = type + } + public init(videoRange: VideoRange) { + self.type = videoRange + } +} + /// Represents a playlist type /// /// Can be initialized with a string "EVENT" or "VOD" for a valid value diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift index 2498a3d..8f6ff0c 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift @@ -435,7 +435,7 @@ extension PantosTag: HLSTagDescriptor, Equatable { HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.frameRate, optional: true, expectedType: Double.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: HLSHDCPLevel.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self), - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: HLSVideoRange.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.stableVariantId, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.audioGroup, optional: true, expectedType: String.self), @@ -476,7 +476,7 @@ extension PantosTag: HLSTagDescriptor, Equatable { HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.resolution, optional: true, expectedType: HLSResolution.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: HLSHDCPLevel.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self), - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: HLSVideoRange.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.stableVariantId, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoGroup, optional: true, expectedType: String.self), diff --git a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index cd9c474..f23233d 100644 --- a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift +++ b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift @@ -761,7 +761,8 @@ class GenericDictionaryTagValidatorTests: XCTestCase { .programId, .resolution, .frameRate, - .hdcpLevel] + .hdcpLevel, + .videoRange] validate(tag: PantosTag.EXT_X_STREAM_INF, tagData: tagData, @@ -910,7 +911,8 @@ class GenericDictionaryTagValidatorTests: XCTestCase { .score, .programId, .resolution, - .hdcpLevel] + .hdcpLevel, + .videoRange] validate(tag: PantosTag.EXT_X_I_FRAME_STREAM_INF, tagData: tagData, From 4fd3f9d2106af2bb28f629787dab9d8e174a3058 Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Mon, 2 Sep 2024 20:59:39 -0400 Subject: [PATCH 10/18] Added parsing for REQ-VIDEO-LAYOUT --- mamba.xcodeproj/project.pbxproj | 8 ++ mambaSharedFramework/HLSValueTypes.swift | 64 ++++++++++++++ .../PantosTag.swift | 4 +- .../GenericDictionaryTagValidatorTests.swift | 6 +- .../Value Types/HLSVideoLayoutTests.swift | 84 +++++++++++++++++++ 5 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 mambaTests/Util Tests/Value Types/HLSVideoLayoutTests.swift diff --git a/mamba.xcodeproj/project.pbxproj b/mamba.xcodeproj/project.pbxproj index f9973dc..157019f 100644 --- a/mamba.xcodeproj/project.pbxproj +++ b/mamba.xcodeproj/project.pbxproj @@ -20,6 +20,9 @@ 144758392C8620C000D12CCD /* HLSChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758382C8620C000D12CCD /* HLSChannelsTests.swift */; }; 1447583A2C8620C000D12CCD /* HLSChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758382C8620C000D12CCD /* HLSChannelsTests.swift */; }; 1447583B2C8620C000D12CCD /* HLSChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758382C8620C000D12CCD /* HLSChannelsTests.swift */; }; + 1447583D2C8693E000D12CCD /* HLSVideoLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1447583C2C8693E000D12CCD /* HLSVideoLayoutTests.swift */; }; + 1447583E2C8693E000D12CCD /* HLSVideoLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1447583C2C8693E000D12CCD /* HLSVideoLayoutTests.swift */; }; + 1447583F2C8693E000D12CCD /* HLSVideoLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1447583C2C8693E000D12CCD /* HLSVideoLayoutTests.swift */; }; 1D28F3451EAA9E500010320B /* hls_ad_master_playlist.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 1D28F3401EAA9E500010320B /* hls_ad_master_playlist.m3u8 */; }; 1D28F3461EAA9E500010320B /* hls_ad_variant_playlist.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 1D28F3411EAA9E500010320B /* hls_ad_variant_playlist.m3u8 */; }; 1D28F3471EAA9E500010320B /* hls_master_playlist_sap.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 1D28F3421EAA9E500010320B /* hls_master_playlist_sap.m3u8 */; }; @@ -630,6 +633,7 @@ 144758302C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXT_X_SESSION_DATATagValidator.swift; sourceTree = ""; }; 144758342C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXT_X_SESSION_DATAPlaylistValidator.swift; sourceTree = ""; }; 144758382C8620C000D12CCD /* HLSChannelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HLSChannelsTests.swift; sourceTree = ""; }; + 1447583C2C8693E000D12CCD /* HLSVideoLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HLSVideoLayoutTests.swift; sourceTree = ""; }; 1D28F3401EAA9E500010320B /* hls_ad_master_playlist.m3u8 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = hls_ad_master_playlist.m3u8; sourceTree = ""; }; 1D28F3411EAA9E500010320B /* hls_ad_variant_playlist.m3u8 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = hls_ad_variant_playlist.m3u8; sourceTree = ""; }; 1D28F3421EAA9E500010320B /* hls_master_playlist_sap.m3u8 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = hls_master_playlist_sap.m3u8; sourceTree = ""; }; @@ -1334,6 +1338,7 @@ EC7492B01DD29F8900AF4E20 /* HLSMediaTypeTests.swift */, EC7492B11DD29F8900AF4E20 /* HLSPlaylistTypeTests.swift */, EC7492B21DD29F8900AF4E20 /* HLSResolutionTests.swift */, + 1447583C2C8693E000D12CCD /* HLSVideoLayoutTests.swift */, ); path = "Value Types"; sourceTree = ""; @@ -1937,6 +1942,7 @@ EC74923E1DD29E7300AF4E20 /* HLSParser_Super8DemuxedTests.swift in Sources */, EC7492781DD29EC800AF4E20 /* EXT_X_MEDIATagParserTests.swift in Sources */, EC6F38921EA95882006BC30E /* HLSPlaylistInterfaceTests.swift in Sources */, + 1447583D2C8693E000D12CCD /* HLSVideoLayoutTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2106,6 +2112,7 @@ EC74923F1DD29E7300AF4E20 /* HLSParser_Super8DemuxedTests.swift in Sources */, EC8A3C801F7C329900A50EED /* HLSPlaylistStructureMasterTests.swift in Sources */, EC7492791DD29EC800AF4E20 /* EXT_X_MEDIATagParserTests.swift in Sources */, + 1447583E2C8693E000D12CCD /* HLSVideoLayoutTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2275,6 +2282,7 @@ ECE253F1209A50B500D388CE /* EXT_X_KEYTagParserTests.swift in Sources */, ECE25403209A50B500D388CE /* String+Helio.swift in Sources */, ECE25400209A50B500D388CE /* IndeterminateBoolTests.swift in Sources */, + 1447583F2C8693E000D12CCD /* HLSVideoLayoutTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/mambaSharedFramework/HLSValueTypes.swift b/mambaSharedFramework/HLSValueTypes.swift index 3afb721..05f0f10 100644 --- a/mambaSharedFramework/HLSValueTypes.swift +++ b/mambaSharedFramework/HLSValueTypes.swift @@ -435,4 +435,68 @@ public func ==(lhs: HLSCodecArray, rhs: HLSCodecArray) -> Bool { return lhs.codecs == rhs.codecs } +/// Represents information to assist in view presentation. +/// +/// Indicates when video content in the Variant Stream requires specialized rendering to be properly displayed. +public struct HLSVideoLayout: Equatable, FailableStringLiteralConvertible { + /// Each specifier controls one aspect of the entry. That is, the specifiers are disjoint and the values for a + /// specifier are mutually exclusive. + public let layouts: [VideoLayout] + /// The client SHOULD assume that the order of entries reflects the most common presentation in the content. + /// + /// For example, if the content is predominantly stereoscopic, with some brief sections that are monoscopic then the + /// Multivariant Playlist SHOULD specify `REQ-VIDEO-LAYOUT="CH-STEREO,CH-MONO"`. On the other hand, if the content + /// is predominantly monoscopic then the Multivariant Playlist SHOULD specify `REQ-VIDEO-LAYOUT="CH-MONO,CH-STEREO"`. + public let predominantLayout: VideoLayout + + public enum VideoLayout: String { + /// Monoscopic. + /// + /// Indicates that a single image is present. + case chMono = "CH-MONO" + /// Stereoscopic. + /// + /// Indicates that both left and right eye images are present. + case chStereo = "CH-STEREO" + + init?(str: Substring) { + switch str { + case "CH-MONO": self = .chMono + case "CH-STEREO": self = .chStereo + default: return nil + } + } + } + public init?(string: String) { + var layouts = [VideoLayout]() + for str in string.split(separator: ",") { + if let layout = VideoLayout(str: str) { + layouts.append(layout) + } else { + // Favor failing to parse the whole array if we find an unrecognized layout, so that we don't risk mis- + // reporting the existing layouts. + return nil + } + } + guard let firstLayout = layouts.first else { + return nil + } + self.predominantLayout = firstLayout + self.layouts = layouts + } + + public init?(layouts: [VideoLayout]) { + guard let predominantLayout = layouts.first else { return nil } + self.layouts = layouts + self.predominantLayout = predominantLayout + } + + public func containsStereo() -> Bool { + layouts.contains(.chStereo) + } + + public func containsMono() -> Bool { + layouts.contains(.chMono) + } +} diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift index 8f6ff0c..6fe30a3 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift @@ -436,7 +436,7 @@ extension PantosTag: HLSTagDescriptor, Equatable { HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: HLSHDCPLevel.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: HLSVideoRange.self), - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: HLSVideoLayout.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.stableVariantId, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.audioGroup, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoGroup, optional: true, expectedType: String.self), @@ -477,7 +477,7 @@ extension PantosTag: HLSTagDescriptor, Equatable { HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: HLSHDCPLevel.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: HLSVideoRange.self), - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: HLSVideoLayout.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.stableVariantId, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoGroup, optional: true, expectedType: String.self), HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.pathwayId, optional: true, expectedType: String.self), diff --git a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index f23233d..80bc682 100644 --- a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift +++ b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift @@ -762,7 +762,8 @@ class GenericDictionaryTagValidatorTests: XCTestCase { .resolution, .frameRate, .hdcpLevel, - .videoRange] + .videoRange, + .reqVideoLayout] validate(tag: PantosTag.EXT_X_STREAM_INF, tagData: tagData, @@ -912,7 +913,8 @@ class GenericDictionaryTagValidatorTests: XCTestCase { .programId, .resolution, .hdcpLevel, - .videoRange] + .videoRange, + .reqVideoLayout] validate(tag: PantosTag.EXT_X_I_FRAME_STREAM_INF, tagData: tagData, diff --git a/mambaTests/Util Tests/Value Types/HLSVideoLayoutTests.swift b/mambaTests/Util Tests/Value Types/HLSVideoLayoutTests.swift new file mode 100644 index 0000000..61a0757 --- /dev/null +++ b/mambaTests/Util Tests/Value Types/HLSVideoLayoutTests.swift @@ -0,0 +1,84 @@ +// +// HLSVideoLayoutTests.swift +// mamba +// +// Created by Robert Galluccio on 9/2/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 +import XCTest +import mamba + +class HLSVideoLayoutTests: XCTestCase { + let empty = "" + let invalidVideoLayout = "CH-TRI" + let monoLayout = "CH-MONO" + let stereoLayout = "CH-STEREO" + let stereoWithMonoLayout = "CH-STEREO,CH-MONO" + let monoWithStereoLayout = "CH-MONO,CH-STEREO" + let monoWithStereoWithUnknownLayout = "CH-MONO,CH-STEREO,CH-TRI" + + func test_empty() { + XCTAssertNil(HLSVideoLayout(string: empty)) + } + + func test_invalidVideoLayout() { + XCTAssertNil(HLSVideoLayout(string: invalidVideoLayout)) + } + + func test_monoLayout() { + guard let videoLayout = HLSVideoLayout(string: monoLayout) else { + return XCTFail("Expected to parse REQ-VIDEO-LAYOUT from \(monoLayout).") + } + XCTAssertEqual(videoLayout.layouts, [.chMono]) + XCTAssertEqual(videoLayout.predominantLayout, .chMono) + XCTAssertTrue(videoLayout.containsMono()) + XCTAssertFalse(videoLayout.containsStereo()) + } + + func test_stereoLayout() { + guard let videoLayout = HLSVideoLayout(string: stereoLayout) else { + return XCTFail("Expected to parse REQ-VIDEO-LAYOUT from \(stereoLayout).") + } + XCTAssertEqual(videoLayout.layouts, [.chStereo]) + XCTAssertEqual(videoLayout.predominantLayout, .chStereo) + XCTAssertFalse(videoLayout.containsMono()) + XCTAssertTrue(videoLayout.containsStereo()) + } + + func test_stereoWithMonoLayout() { + guard let videoLayout = HLSVideoLayout(string: stereoWithMonoLayout) else { + return XCTFail("Expected to parse REQ-VIDEO-LAYOUT from \(stereoWithMonoLayout).") + } + XCTAssertEqual(videoLayout.layouts, [.chStereo, .chMono]) + XCTAssertEqual(videoLayout.predominantLayout, .chStereo) + XCTAssertTrue(videoLayout.containsMono()) + XCTAssertTrue(videoLayout.containsStereo()) + } + + func test_monoWithStereoLayout() { + guard let videoLayout = HLSVideoLayout(string: monoWithStereoLayout) else { + return XCTFail("Expected to parse REQ-VIDEO-LAYOUT from \(monoWithStereoLayout).") + } + XCTAssertEqual(videoLayout.layouts, [.chMono, .chStereo]) + XCTAssertEqual(videoLayout.predominantLayout, .chMono) + XCTAssertTrue(videoLayout.containsMono()) + XCTAssertTrue(videoLayout.containsStereo()) + } + + func test_monoWithStereoWithUnknownLayout() { + XCTAssertNil(HLSVideoLayout(string: monoWithStereoWithUnknownLayout)) + } +} From 32d0ec83aa2026d99e35b020688c9c826eda066e Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Mon, 2 Sep 2024 21:06:31 -0400 Subject: [PATCH 11/18] Added parsing for FORMAT in EXT-X-SESSION-DATA --- mambaSharedFramework/HLSValueTypes.swift | 23 +++++++++++++++++-- .../EXT_X_SESSION_DATATagValidator.swift | 2 +- .../GenericDictionaryTagValidatorTests.swift | 6 ++--- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/mambaSharedFramework/HLSValueTypes.swift b/mambaSharedFramework/HLSValueTypes.swift index 05f0f10..f379f29 100644 --- a/mambaSharedFramework/HLSValueTypes.swift +++ b/mambaSharedFramework/HLSValueTypes.swift @@ -196,6 +196,27 @@ public struct HLSVideoRange: Equatable, FailableStringLiteralConvertible { } } +/// Represents the format of the file referenced by `EXT-X-SESSION-DATA:URI`. +public struct HLSSessionDataFormat: Equatable, FailableStringLiteralConvertible { + public let type: Format + public enum Format: String { + case json = "JSON" + case raw = "RAW" + } + public init?(string: String) { + self.init(format: string) + } + public init?(format: String) { + guard let type = Format(rawValue: format) else { + return nil + } + self.type = type + } + public init(format: Format) { + self.type = format + } +} + /// Represents a playlist type /// /// Can be initialized with a string "EVENT" or "VOD" for a valid value @@ -226,7 +247,6 @@ public func ==(lhs: HLSPlaylistType, rhs: HLSPlaylistType) -> Bool { /// Represents a instreamId type /// /// Can be initialized with a string "CC1" or "CC2" or "CC3" or "CC4" for a valid value - public enum HLSInstreamId: String, FailableStringLiteralConvertible { case CC1 = "CC1" case CC2 = "CC2" @@ -239,7 +259,6 @@ public enum HLSInstreamId: String, FailableStringLiteralConvertible { } - /// Represents a CLOSED-CAPTIONS /// /// can be either a quoted-string or an enumerated-string with the value NONE for a valid value diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift index f3d4d70..499eb63 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift @@ -44,7 +44,7 @@ class EXT_X_SESSION_DATATagValidator: HLSTagValidator { HLSDictionaryTagValueIdentifierImpl( valueId: PantosValue.format, optional: true, - expectedType: String.self + expectedType: HLSSessionDataFormat.self ), HLSDictionaryTagValueIdentifierImpl( valueId: PantosValue.language, diff --git a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index 80bc682..987866c 100644 --- a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift +++ b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift @@ -986,14 +986,14 @@ class GenericDictionaryTagValidatorTests: XCTestCase { tagData: withURI, optional: [.value, .format, .language], mandatory: [.dataId, .uri], - badValues: []) - + badValues: [.format]) + let withValue = "DATA-ID=\"com.example.data\",VALUE=\"Hello, World!\",LANGUAGE=\"en\"" validate(tag: PantosTag.EXT_X_SESSION_DATA, tagData: withValue, optional: [.uri, .format, .language], mandatory: [.dataId, .value], - badValues: []) + badValues: [.format]) // Using a closure to avoid naming clashes in the rest of the test. let EXT_X_SESSION_DATA_withNoValueOrURI = { From cdd2694b4feeff4fdc34769862c82f718a2eeb60 Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Mon, 2 Sep 2024 23:39:29 -0400 Subject: [PATCH 12/18] Corrected location for EXT-X-SESSION-DATA playlist validation --- .../HLSPlaylistValidatorImpl.swift | 6 +++--- mambaTests/HLSValidatorTests.swift | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/HLSPlaylistValidatorImpl.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/HLSPlaylistValidatorImpl.swift index 6a3d134..4075558 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/HLSPlaylistValidatorImpl.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/HLSPlaylistValidatorImpl.swift @@ -76,7 +76,8 @@ public class HLSMasterPlaylistValidator: HLSExtensibleValidator { public static let validators:[HLSValidator.Type] = [HLSPlaylistRenditionGroupValidator.self, EXT_X_STREAM_INFRenditionGroupAUDIOValidator.self, EXT_X_STREAM_INFRenditionGroupVIDEOValidator.self, - EXT_X_STREAM_INFRenditionGroupSUBTITLESValidator.self] + EXT_X_STREAM_INFRenditionGroupSUBTITLESValidator.self, + EXT_X_SESSION_DATAPlaylistValidator.self] } /// Validator for variant playlists @@ -86,8 +87,7 @@ public class HLSVariantPlaylistValidator: HLSExtensibleValidator { HLSPlaylistRenditionGroupMatchingPROGRAM_IDValidator.self, HLSPlaylistRenditionGroupMatchingNAMELANGUAGEValidator.self, EXT_X_STARTTimeOffsetValidator.self, - EXT_X_DATERANGEPlaylistValidator.self, - EXT_X_SESSION_DATAPlaylistValidator.self] + EXT_X_DATERANGEPlaylistValidator.self] } /// A general purpose validator that will validate either a variant or a master playlist diff --git a/mambaTests/HLSValidatorTests.swift b/mambaTests/HLSValidatorTests.swift index cd7b635..260e8ac 100644 --- a/mambaTests/HLSValidatorTests.swift +++ b/mambaTests/HLSValidatorTests.swift @@ -810,4 +810,15 @@ frag1.ts ) } + func testEXT_X_SESSION_DATAPlaylistValidator_existsWithinMasterPlaylistValidators() { + XCTAssertEqual( + 1, + HLSMasterPlaylistValidator.validators.filter { $0 == EXT_X_SESSION_DATAPlaylistValidator.self }.count + ) + XCTAssertEqual( + 0, + HLSVariantPlaylistValidator.validators.filter { $0 == EXT_X_SESSION_DATAPlaylistValidator.self }.count + ) + } + } From c871de1e2999716474282d8d8706622ca2581dfb Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Fri, 6 Sep 2024 22:46:28 -0400 Subject: [PATCH 13/18] Favor structs over classes --- .../EXT_X_SESSION_DATATagValidator.swift | 2 +- .../EXT_X_SESSION_KEYValidator.swift | 27 ++++++++++++++++--- .../PantosTag.swift | 18 +------------ 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift index 499eb63..44e347b 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift @@ -19,7 +19,7 @@ import Foundation -class EXT_X_SESSION_DATATagValidator: HLSTagValidator { +struct EXT_X_SESSION_DATATagValidator: HLSTagValidator { private var genericValidator: GenericDictionaryTagValidator init() { diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift index e814e72..98828e4 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift @@ -21,10 +21,31 @@ import Foundation // All attributes defined for the EXT-X-KEY tag (Section 4.4.4.4) are also defined for the // EXT-X-SESSION-KEY, except that the value of the METHOD attribute MUST NOT be NONE. -class EXT_X_SESSION_KEYValidator: EXT_X_KEYValidator { +struct EXT_X_SESSION_KEYValidator: HLSTagValidator { + private let keyValidator: EXT_X_KEYValidator - override public func validate(tag: HLSTag) -> [HLSValidationIssue]? { - var issueList = super.validate(tag: tag) ?? [] + init() { + keyValidator = EXT_X_KEYValidator(tag: PantosTag.EXT_X_SESSION_KEY, dictionaryValueIdentifiers: [ + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.method, + optional: false, + expectedType: HLSEncryptionMethodType.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.uri, + optional: false, // URI is REQUIRED since METHOD can't be NONE + expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.ivector, + optional: true, + expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.keyformat, + optional: true, + expectedType: String.self), + HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.keyformatVersions, + optional: true, + expectedType: String.self) + ]) + } + + public func validate(tag: HLSTag) -> [HLSValidationIssue]? { + var issueList = keyValidator.validate(tag: tag) ?? [] if let method = tag.value(forValueIdentifier: PantosValue.method) { if method == HLSEncryptionMethodType.EncryptionMethod.None.rawValue { diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift index d6ea28b..6b328e8 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/PantosTag.swift @@ -496,23 +496,7 @@ extension PantosTag: HLSTagDescriptor, Equatable { return EXT_X_SESSION_DATATagValidator() case .EXT_X_SESSION_KEY: - return EXT_X_SESSION_KEYValidator(tag: pantostag, dictionaryValueIdentifiers: [ - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.method, - optional: false, - expectedType: HLSEncryptionMethodType.self), - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.uri, - optional: false, // URI is REQUIRED since METHOD can't be NONE - expectedType: String.self), - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.ivector, - optional: true, - expectedType: String.self), - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.keyformat, - optional: true, - expectedType: String.self), - HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.keyformatVersions, - optional: true, - expectedType: String.self) - ]) + return EXT_X_SESSION_KEYValidator() case .EXT_X_CONTENT_STEERING: return GenericDictionaryTagValidator(tag: pantostag, dictionaryValueIdentifiers: [ From f23275d35b6e50ab7578dc991b853d6d4255ec81 Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Fri, 6 Sep 2024 22:49:36 -0400 Subject: [PATCH 14/18] Favor not double declaring the issue variable --- .../EXT_X_SESSION_DATAPlaylistValidator.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift index 79ad76c..e35cf18 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift @@ -23,8 +23,9 @@ class EXT_X_SESSION_DATAPlaylistValidator: HLSPlaylistValidator { static func validate(hlsPlaylist: any HLSPlaylistInterface) -> [HLSValidationIssue]? { var issues = [HLSValidationIssue]() - let issue = duplicateIssue(tags: hlsPlaylist.tags.filter { $0.tagDescriptor == PantosTag.EXT_X_SESSION_DATA }) - if let issue { + if let issue = duplicateIssue( + tags: hlsPlaylist.tags.filter { $0.tagDescriptor == PantosTag.EXT_X_SESSION_DATA } + ) { issues.append(issue) } From 440593faa16ffa74b52799acc8a957c5f6eaaf9b Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Fri, 6 Sep 2024 22:58:23 -0400 Subject: [PATCH 15/18] Clarified enum naming and favored more terse function syntax --- mambaSharedFramework/HLSValueTypes.swift | 20 ++++++++----------- .../Value Types/HLSVideoLayoutTests.swift | 16 +++++++-------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/mambaSharedFramework/HLSValueTypes.swift b/mambaSharedFramework/HLSValueTypes.swift index f379f29..bf39df7 100644 --- a/mambaSharedFramework/HLSValueTypes.swift +++ b/mambaSharedFramework/HLSValueTypes.swift @@ -460,15 +460,15 @@ public func ==(lhs: HLSCodecArray, rhs: HLSCodecArray) -> Bool { public struct HLSVideoLayout: Equatable, FailableStringLiteralConvertible { /// Each specifier controls one aspect of the entry. That is, the specifiers are disjoint and the values for a /// specifier are mutually exclusive. - public let layouts: [VideoLayout] + public let layouts: [VideoLayoutIdentifier] /// The client SHOULD assume that the order of entries reflects the most common presentation in the content. /// /// For example, if the content is predominantly stereoscopic, with some brief sections that are monoscopic then the /// Multivariant Playlist SHOULD specify `REQ-VIDEO-LAYOUT="CH-STEREO,CH-MONO"`. On the other hand, if the content /// is predominantly monoscopic then the Multivariant Playlist SHOULD specify `REQ-VIDEO-LAYOUT="CH-MONO,CH-STEREO"`. - public let predominantLayout: VideoLayout + public let predominantLayout: VideoLayoutIdentifier - public enum VideoLayout: String { + public enum VideoLayoutIdentifier: String { /// Monoscopic. /// /// Indicates that a single image is present. @@ -488,9 +488,9 @@ public struct HLSVideoLayout: Equatable, FailableStringLiteralConvertible { } public init?(string: String) { - var layouts = [VideoLayout]() + var layouts = [VideoLayoutIdentifier]() for str in string.split(separator: ",") { - if let layout = VideoLayout(str: str) { + if let layout = VideoLayoutIdentifier(str: str) { layouts.append(layout) } else { // Favor failing to parse the whole array if we find an unrecognized layout, so that we don't risk mis- @@ -505,17 +505,13 @@ public struct HLSVideoLayout: Equatable, FailableStringLiteralConvertible { self.layouts = layouts } - public init?(layouts: [VideoLayout]) { + public init?(layouts: [VideoLayoutIdentifier]) { guard let predominantLayout = layouts.first else { return nil } self.layouts = layouts self.predominantLayout = predominantLayout } - public func containsStereo() -> Bool { - layouts.contains(.chStereo) - } - - public func containsMono() -> Bool { - layouts.contains(.chMono) + public func contains(_ layout: VideoLayoutIdentifier) -> Bool { + layouts.contains(layout) } } diff --git a/mambaTests/Util Tests/Value Types/HLSVideoLayoutTests.swift b/mambaTests/Util Tests/Value Types/HLSVideoLayoutTests.swift index 61a0757..fafa7ed 100644 --- a/mambaTests/Util Tests/Value Types/HLSVideoLayoutTests.swift +++ b/mambaTests/Util Tests/Value Types/HLSVideoLayoutTests.swift @@ -44,8 +44,8 @@ class HLSVideoLayoutTests: XCTestCase { } XCTAssertEqual(videoLayout.layouts, [.chMono]) XCTAssertEqual(videoLayout.predominantLayout, .chMono) - XCTAssertTrue(videoLayout.containsMono()) - XCTAssertFalse(videoLayout.containsStereo()) + XCTAssertTrue(videoLayout.contains(.chMono)) + XCTAssertFalse(videoLayout.contains(.chStereo)) } func test_stereoLayout() { @@ -54,8 +54,8 @@ class HLSVideoLayoutTests: XCTestCase { } XCTAssertEqual(videoLayout.layouts, [.chStereo]) XCTAssertEqual(videoLayout.predominantLayout, .chStereo) - XCTAssertFalse(videoLayout.containsMono()) - XCTAssertTrue(videoLayout.containsStereo()) + XCTAssertFalse(videoLayout.contains(.chMono)) + XCTAssertTrue(videoLayout.contains(.chStereo)) } func test_stereoWithMonoLayout() { @@ -64,8 +64,8 @@ class HLSVideoLayoutTests: XCTestCase { } XCTAssertEqual(videoLayout.layouts, [.chStereo, .chMono]) XCTAssertEqual(videoLayout.predominantLayout, .chStereo) - XCTAssertTrue(videoLayout.containsMono()) - XCTAssertTrue(videoLayout.containsStereo()) + XCTAssertTrue(videoLayout.contains(.chMono)) + XCTAssertTrue(videoLayout.contains(.chStereo)) } func test_monoWithStereoLayout() { @@ -74,8 +74,8 @@ class HLSVideoLayoutTests: XCTestCase { } XCTAssertEqual(videoLayout.layouts, [.chMono, .chStereo]) XCTAssertEqual(videoLayout.predominantLayout, .chMono) - XCTAssertTrue(videoLayout.containsMono()) - XCTAssertTrue(videoLayout.containsStereo()) + XCTAssertTrue(videoLayout.contains(.chMono)) + XCTAssertTrue(videoLayout.contains(.chStereo)) } func test_monoWithStereoWithUnknownLayout() { From 34de4a993d2206f793afe18e53db83b07d7f8ca8 Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Fri, 6 Sep 2024 23:09:52 -0400 Subject: [PATCH 16/18] Favor enums rather than structs holding enums --- mambaSharedFramework/HLSValueTypes.swift | 95 ++++++++---------------- 1 file changed, 31 insertions(+), 64 deletions(-) diff --git a/mambaSharedFramework/HLSValueTypes.swift b/mambaSharedFramework/HLSValueTypes.swift index bf39df7..378c86a 100644 --- a/mambaSharedFramework/HLSValueTypes.swift +++ b/mambaSharedFramework/HLSValueTypes.swift @@ -132,29 +132,18 @@ public func ==(lhs: HLSEncryptionMethodType, rhs: HLSEncryptionMethodType) -> Bo } /// Represents a minimum required HDCP level needed to play content. -public struct HLSHDCPLevel: Equatable, FailableStringLiteralConvertible { - public let type: HDCPLevel - public enum HDCPLevel: String { - /// Indicates that the content does not require output copy protections. - case none = "NONE" - /// Indicates that the Variant Stream could fail to play unless the output is protected by High-bandwidth - /// Digital Content Protection (HDCP) Type 0 or equivalent. - case type0 = "TYPE-0" - /// Indicates that the Variant Stream could fail to play unless the output is protected by HDCP Type 1 or - /// equivalent. - case type1 = "TYPE-1" - } +public enum HLSHDCPLevel: String, Equatable, FailableStringLiteralConvertible { + /// Indicates that the content does not require output copy protections. + case none = "NONE" + /// Indicates that the Variant Stream could fail to play unless the output is protected by High-bandwidth Digital + /// Content Protection (HDCP) Type 0 or equivalent. + case type0 = "TYPE-0" + /// Indicates that the Variant Stream could fail to play unless the output is protected by HDCP Type 1 or + /// equivalent. + case type1 = "TYPE-1" + public init?(string: String) { - self.init(hdcpLevel: string) - } - public init?(hdcpLevel: String) { - guard let type = HDCPLevel(rawValue: hdcpLevel) else { - return nil - } - self.type = type - } - public init(hdcpLevel: HDCPLevel) { - self.type = hdcpLevel + self.init(rawValue: string) } } @@ -166,54 +155,32 @@ public struct HLSHDCPLevel: Equatable, FailableStringLiteralConvertible { /// For example, `SDR` covers TransferCharacteristics code points 1, 6, 13, 14 and 15. More information on what each /// code point represents can be found in _"Information technology - MPEG systems technologies - Part 8: Coding-_ /// _independent code points" ISO/IEC International Standard 23001-8, 2016_ [CICP]. -public struct HLSVideoRange: Equatable, FailableStringLiteralConvertible { - public let type: VideoRange - public enum VideoRange: String { - /// The value MUST be SDR if the video in the Variant Stream is encoded using one of the following reference - /// opto-electronic transfer characteristic functions specified by the TransferCharacteristics code point: 1, 6, - /// 13, 14, 15. Note that different TransferCharacteristics code points can use the same transfer function. - case sdr = "SDR" - /// The value MUST be HLG if the video in the Variant Stream is encoded using a reference opto-electronic - /// transfer characteristic function specified by the TransferCharacteristics code point 18, or consists of such - /// video mixed with video qualifying as SDR. - case hlg = "HLG" - /// The value MUST be PQ if the video in the Variant Stream is encoded using a reference opto-electronic - /// transfer characteristic function specified by the TransferCharacteristics code point 16, or consists of such - /// video mixed with video qualifying as SDR or HLG. - case pq = "PQ" - } +public enum HLSVideoRange: String, Equatable, FailableStringLiteralConvertible { + /// The value MUST be SDR if the video in the Variant Stream is encoded using one of the following reference + /// opto-electronic transfer characteristic functions specified by the TransferCharacteristics code point: 1, 6, 13, + /// 14, 15. Note that different TransferCharacteristics code points can use the same transfer function. + case sdr = "SDR" + /// The value MUST be HLG if the video in the Variant Stream is encoded using a reference opto-electronic transfer + /// characteristic function specified by the TransferCharacteristics code point 18, or consists of such video mixed + /// with video qualifying as SDR. + case hlg = "HLG" + /// The value MUST be PQ if the video in the Variant Stream is encoded using a reference opto-electronic transfer + /// characteristic function specified by the TransferCharacteristics code point 16, or consists of such video mixed + /// with video qualifying as SDR or HLG. + case pq = "PQ" + public init?(string: String) { - self.init(videoRange: string) - } - public init?(videoRange: String) { - guard let type = VideoRange(rawValue: videoRange) else { - return nil - } - self.type = type - } - public init(videoRange: VideoRange) { - self.type = videoRange + self.init(rawValue: string) } } /// Represents the format of the file referenced by `EXT-X-SESSION-DATA:URI`. -public struct HLSSessionDataFormat: Equatable, FailableStringLiteralConvertible { - public let type: Format - public enum Format: String { - case json = "JSON" - case raw = "RAW" - } +public enum HLSSessionDataFormat: String, Equatable, FailableStringLiteralConvertible { + case json = "JSON" + case raw = "RAW" + public init?(string: String) { - self.init(format: string) - } - public init?(format: String) { - guard let type = Format(rawValue: format) else { - return nil - } - self.type = type - } - public init(format: Format) { - self.type = format + self.init(rawValue: string) } } From 02d2cb9f8a36379b6b1d49891da3bae364c1309c Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Fri, 6 Sep 2024 23:45:37 -0400 Subject: [PATCH 17/18] Introduce unrecognized cases to keep parsing forward compatible --- mambaSharedFramework/HLSValueTypes.swift | 86 ++++++++++--------- .../GenericDictionaryTagValidatorTests.swift | 6 +- .../Value Types/HLSChannelsTests.swift | 10 ++- .../Value Types/HLSVideoLayoutTests.swift | 26 ++++-- 4 files changed, 74 insertions(+), 54 deletions(-) diff --git a/mambaSharedFramework/HLSValueTypes.swift b/mambaSharedFramework/HLSValueTypes.swift index 378c86a..7d17b95 100644 --- a/mambaSharedFramework/HLSValueTypes.swift +++ b/mambaSharedFramework/HLSValueTypes.swift @@ -255,25 +255,41 @@ public struct HLSChannels: Equatable, FailableStringLiteralConvertible { /// This parameter is an array of Special Usage Identifiers. public let specialUsageIdentifiers: [SpecialUsageIdentifier] - public enum SpecialUsageIdentifier: String { + public enum SpecialUsageIdentifier: RawRepresentable, Equatable { /// The audio is binaural (either recorded or synthesized). It SHOULD NOT be dynamically spatialized. It is best /// suited for delivery to headphones. - case binaural = "BINAURAL" + case binaural /// The audio is pre-processed content that SHOULD NOT be dynamically spatialized. It is suitable to deliver to /// either headphones or speakers. - case immersive = "IMMERSIVE" + case immersive /// The audio is a downmix derivative of some other audio. If desired, the downmix may be used as a subtitute /// for alternative Renditions in the same group with compatible attributes and a greater channel count. It MAY /// be dynamically spatialized. - case downmix = "DOWNMIX" + case downmix + /// The audio identifier is not recognized by this library; however, we provide the raw identifier string that + /// existed in the manifest. + case unrecognized(String) + + public var rawValue: String { + switch self { + case .binaural: return "BINAURAL" + case .immersive: return "IMMERSIVE" + case .downmix: return "DOWNMIX" + case .unrecognized(let string): return string + } + } + + public init?(rawValue: String) { + self.init(str: Substring(rawValue)) + } /// Allows `init` without having to allocate a new `String` object. - init?(str: Substring) { + init(str: Substring) { switch str { case "BINAURAL": self = .binaural case "IMMERSIVE": self = .immersive case "DOWNMIX": self = .downmix - default: return nil + default: self = .unrecognized(String(str)) } } } @@ -287,15 +303,7 @@ public struct HLSChannels: Equatable, FailableStringLiteralConvertible { switch index { case 0: count = Self.parseChannelCount(str: str) case 1: spatialAudioCodingIdentifiers = Self.parseSpatialAudioCodingIdentifiers(str: str) - case 2: - guard let ids = Self.parseSpecialUsageIdentifiers(str: str) else { - // In the case that we don't recognize one of the special usage identifiers, leading to nil being - // parsed out, I believe it is better to fail the entire parsing, as otherwise we could mislead the - // user of the library into thinking that there are less special usage identifiers than there - // actually are in the CHANNELS attribtue. - return nil - } - specialUsageIdentifiers = ids + case 2: specialUsageIdentifiers = Self.parseSpecialUsageIdentifiers(str: str) default: break // In the future there may be more parameters defined. } } @@ -331,16 +339,8 @@ public struct HLSChannels: Equatable, FailableStringLiteralConvertible { return identifiers } - private static func parseSpecialUsageIdentifiers(str: Substring) -> [SpecialUsageIdentifier]? { - let split = str.split(separator: ",") - var identifiers = [SpecialUsageIdentifier]() - for id in split { - guard let specialUsageId = SpecialUsageIdentifier(str: id) else { - return nil - } - identifiers.append(specialUsageId) - } - return identifiers + private static func parseSpecialUsageIdentifiers(str: Substring) -> [SpecialUsageIdentifier] { + str.split(separator: ",").map { SpecialUsageIdentifier(str: $0) } } } @@ -435,36 +435,42 @@ public struct HLSVideoLayout: Equatable, FailableStringLiteralConvertible { /// is predominantly monoscopic then the Multivariant Playlist SHOULD specify `REQ-VIDEO-LAYOUT="CH-MONO,CH-STEREO"`. public let predominantLayout: VideoLayoutIdentifier - public enum VideoLayoutIdentifier: String { + public enum VideoLayoutIdentifier: RawRepresentable, Equatable { /// Monoscopic. /// /// Indicates that a single image is present. - case chMono = "CH-MONO" + case chMono /// Stereoscopic. /// /// Indicates that both left and right eye images are present. - case chStereo = "CH-STEREO" + case chStereo + /// The video layout identifier is not recognized by this library; however, we provide the raw identifier string + /// that existed in the manifest. + case unrecognized(String) + + public var rawValue: String { + switch self { + case .chMono: return "CH-MONO" + case .chStereo: return "CH-STEREO" + case .unrecognized(let string): return string + } + } + + public init?(rawValue: String) { + self.init(str: Substring(rawValue)) + } - init?(str: Substring) { + init(str: Substring) { switch str { case "CH-MONO": self = .chMono case "CH-STEREO": self = .chStereo - default: return nil + default: self = .unrecognized(String(str)) } } } public init?(string: String) { - var layouts = [VideoLayoutIdentifier]() - for str in string.split(separator: ",") { - if let layout = VideoLayoutIdentifier(str: str) { - layouts.append(layout) - } else { - // Favor failing to parse the whole array if we find an unrecognized layout, so that we don't risk mis- - // reporting the existing layouts. - return nil - } - } + let layouts = string.split(separator: ",").map { VideoLayoutIdentifier(str: $0) } guard let firstLayout = layouts.first else { return nil } diff --git a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index 031902d..81d3064 100644 --- a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift +++ b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift @@ -762,8 +762,7 @@ class GenericDictionaryTagValidatorTests: XCTestCase { .resolution, .frameRate, .hdcpLevel, - .videoRange, - .reqVideoLayout] + .videoRange] validate(tag: PantosTag.EXT_X_STREAM_INF, tagData: tagData, @@ -913,8 +912,7 @@ class GenericDictionaryTagValidatorTests: XCTestCase { .programId, .resolution, .hdcpLevel, - .videoRange, - .reqVideoLayout] + .videoRange] validate(tag: PantosTag.EXT_X_I_FRAME_STREAM_INF, tagData: tagData, diff --git a/mambaTests/Util Tests/Value Types/HLSChannelsTests.swift b/mambaTests/Util Tests/Value Types/HLSChannelsTests.swift index 9807edf..57c4780 100644 --- a/mambaTests/Util Tests/Value Types/HLSChannelsTests.swift +++ b/mambaTests/Util Tests/Value Types/HLSChannelsTests.swift @@ -124,11 +124,13 @@ class HLSChannelsTests: XCTestCase { XCTAssertEqual(expectedChannels, actualChannels) } - // In the case that we don't recognize the special usage identifier, I think it is better to fail parsing the entire - // CHANNELS attribute, as otherwise we risk misleading the user of the library into thinking that the special usage - // is less than it actually is. func test_sixChannelUnknownSpecialUsageIdentifier() { let actualChannels = HLSChannels(string: sixChannelUnknownSpecialUsageIdentifier) - XCTAssertNil(actualChannels) + let expectedChannels = HLSChannels( + count: 6, + spatialAudioCodingIdentifiers: [], + specialUsageIdentifiers: [.unrecognized("NEW-IDENTIFIER")] + ) + XCTAssertEqual(expectedChannels, actualChannels) } } diff --git a/mambaTests/Util Tests/Value Types/HLSVideoLayoutTests.swift b/mambaTests/Util Tests/Value Types/HLSVideoLayoutTests.swift index fafa7ed..0b865b1 100644 --- a/mambaTests/Util Tests/Value Types/HLSVideoLayoutTests.swift +++ b/mambaTests/Util Tests/Value Types/HLSVideoLayoutTests.swift @@ -23,19 +23,26 @@ import mamba class HLSVideoLayoutTests: XCTestCase { let empty = "" - let invalidVideoLayout = "CH-TRI" + let unrecognizedVideoLayout = "CH-TRI" let monoLayout = "CH-MONO" let stereoLayout = "CH-STEREO" let stereoWithMonoLayout = "CH-STEREO,CH-MONO" let monoWithStereoLayout = "CH-MONO,CH-STEREO" - let monoWithStereoWithUnknownLayout = "CH-MONO,CH-STEREO,CH-TRI" + let monoWithStereoWithUnrecognizedLayout = "CH-MONO,CH-STEREO,CH-TRI" func test_empty() { XCTAssertNil(HLSVideoLayout(string: empty)) } - func test_invalidVideoLayout() { - XCTAssertNil(HLSVideoLayout(string: invalidVideoLayout)) + func test_unrecognizedVideoLayout() { + guard let videoLayout = HLSVideoLayout(string: unrecognizedVideoLayout) else { + return XCTFail("Expected to parse REQ-VIDEO-LAYOUT from \(unrecognizedVideoLayout).") + } + XCTAssertEqual(videoLayout.layouts, [.unrecognized(unrecognizedVideoLayout)]) + XCTAssertEqual(videoLayout.predominantLayout, .unrecognized(unrecognizedVideoLayout)) + XCTAssertFalse(videoLayout.contains(.chMono)) + XCTAssertFalse(videoLayout.contains(.chStereo)) + XCTAssertTrue(videoLayout.contains(.unrecognized(unrecognizedVideoLayout))) } func test_monoLayout() { @@ -78,7 +85,14 @@ class HLSVideoLayoutTests: XCTestCase { XCTAssertTrue(videoLayout.contains(.chStereo)) } - func test_monoWithStereoWithUnknownLayout() { - XCTAssertNil(HLSVideoLayout(string: monoWithStereoWithUnknownLayout)) + func test_monoWithStereoWithUnrecognizedLayout() { + guard let videoLayout = HLSVideoLayout(string: monoWithStereoWithUnrecognizedLayout) else { + return XCTFail("Expected to parse REQ-VIDEO-LAYOUT from \(monoWithStereoWithUnrecognizedLayout).") + } + XCTAssertEqual(videoLayout.layouts, [.chMono, .chStereo, .unrecognized("CH-TRI")]) + XCTAssertEqual(videoLayout.predominantLayout, .chMono) + XCTAssertTrue(videoLayout.contains(.chMono)) + XCTAssertTrue(videoLayout.contains(.chStereo)) + XCTAssertTrue(videoLayout.contains(.unrecognized("CH-TRI"))) } } From 2d21125811ccc6a92f4603ea61510b03b8a6662c Mon Sep 17 00:00:00 2001 From: theRealRobG Date: Mon, 9 Sep 2024 17:26:53 -0400 Subject: [PATCH 18/18] Explicitly indicate class is final --- .../EXT_X_SESSION_DATAPlaylistValidator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift index e35cf18..618a98c 100644 --- a/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift +++ b/mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift @@ -19,7 +19,7 @@ import Foundation -class EXT_X_SESSION_DATAPlaylistValidator: HLSPlaylistValidator { +final class EXT_X_SESSION_DATAPlaylistValidator: HLSPlaylistValidator { static func validate(hlsPlaylist: any HLSPlaylistInterface) -> [HLSValidationIssue]? { var issues = [HLSValidationIssue]()