From bc451b47e60ccf665ccf2c38cd64769c49bbad71 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 Conflicts: mamba.xcodeproj/project.pbxproj mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/HLSPlaylistValidatorImpl.swift mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift mambaSharedFramework/PlaylistValidationIssue.swift --- mamba.xcodeproj/project.pbxproj | 24 ++ .../EXT_X_SESSION_DATAPlaylistValidator.swift | 53 +++++ .../EXT_X_SESSION_DATATagValidator.swift | 71 ++++++ .../EXT_X_SESSION_KEYValidator.swift | 42 ++++ .../PlaylistValidator.swift | 5 +- .../PantosTag.swift | 66 +++++- .../PantosValue.swift | 21 +- .../PlaylistValidationIssue.swift | 3 + mambaTests/PantosTagTests.swift | 9 + .../GenericDictionaryTagValidatorTests.swift | 207 +++++++++++++++++- mambaTests/ValidatorTests.swift | 48 +++- 11 files changed, 538 insertions(+), 11 deletions(-) create mode 100644 mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift create mode 100644 mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift create mode 100644 mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift diff --git a/mamba.xcodeproj/project.pbxproj b/mamba.xcodeproj/project.pbxproj index 5a182e5..cd33869 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 */; }; @@ -635,6 +644,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 = ""; }; @@ -930,6 +942,9 @@ 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 */, @@ -1773,6 +1788,7 @@ EC7491811DD29C3500AF4E20 /* String+Trim.swift in Sources */, EC7491C31DD29D5C00AF4E20 /* PlaylistValidationIssue.swift in Sources */, ECDE184C22383230008566BB /* PlaylistParser.swift in Sources */, + 144758352C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */, EC74916E1DD29B5D00AF4E20 /* CollectionType+FindExtensions.swift in Sources */, EC7491DA1DD29D9600AF4E20 /* GenericNoDataTagParser.swift in Sources */, EC7491C91DD29D5C00AF4E20 /* PlaylistWriter.swift in Sources */, @@ -1847,6 +1863,8 @@ EC349ACE2236C3A60077432B /* PlaylistStructureInterface.swift in Sources */, EC3B01A71DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupAUTOSELECTValidator.swift in Sources */, F700CD391E78A2BE001C9487 /* MambaStringRef_ConcreteNSString.m in Sources */, + 144758312C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift in Sources */, + 1447582D2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift in Sources */, 43DE4EFB1E564DA300EEE800 /* EXT_X_STARTTimeOffsetValidator.swift in Sources */, EC74918A1DD29CCB00AF4E20 /* StringDictionaryParser.swift in Sources */, ECDE184022381146008566BB /* MasterPlaylist.swift in Sources */, @@ -1945,6 +1963,7 @@ EC3B01AA1DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupDEFAULTValidator.swift in Sources */, EC3B01C41DD4D49A00B512E3 /* PlaylistOneToManyValidator.swift in Sources */, ECDE184D22383230008566BB /* PlaylistParser.swift in Sources */, + 144758362C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */, EC7491821DD29C3500AF4E20 /* String+Trim.swift in Sources */, EC7491C41DD29D5C00AF4E20 /* PlaylistValidationIssue.swift in Sources */, EC74916F1DD29B5D00AF4E20 /* CollectionType+FindExtensions.swift in Sources */, @@ -2019,6 +2038,8 @@ EC349ACF2236C3A60077432B /* PlaylistStructureInterface.swift in Sources */, EC7491471DD299B400AF4E20 /* PlaylistTypes.swift in Sources */, F700CD3A1E78A2BE001C9487 /* MambaStringRef_ConcreteNSString.m in Sources */, + 144758322C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift in Sources */, + 1447582E2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift in Sources */, EC3B01A81DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupAUTOSELECTValidator.swift in Sources */, EC74918B1DD29CCB00AF4E20 /* StringDictionaryParser.swift in Sources */, ECDE184122381146008566BB /* MasterPlaylist.swift in Sources */, @@ -2118,6 +2139,7 @@ EC1CCD32209A2CF9006B59FF /* String+Trim.swift in Sources */, EC1CCD46209A2CF9006B59FF /* GenericSingleTagValidator.swift in Sources */, ECDE184E22383230008566BB /* PlaylistParser.swift in Sources */, + 144758372C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */, EC1CCD30209A2CF9006B59FF /* String+DateParsing.swift in Sources */, EC1CCD53209A2CF9006B59FF /* GenericDictionaryTagWriter.swift in Sources */, EC1CCD55209A2CF9006B59FF /* GenericTagWriter.swift in Sources */, @@ -2189,7 +2211,9 @@ EC1CCD4F209A2CF9006B59FF /* PlaylistTagCardinalityValidation.swift in Sources */, EC1CCD2B209A2CF9006B59FF /* IndeterminateBool.swift in Sources */, EC349AD02236C3A60077432B /* PlaylistStructureInterface.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 /* MambaStringRef_ConcreteNSString.m in Sources */, EC1CCD38209A2CF9006B59FF /* GenericNoDataTagParser.swift in Sources */, diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift new file mode 100644 index 0000000..c687f6f --- /dev/null +++ b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift @@ -0,0 +1,53 @@ +// +// 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: MasterPlaylistValidator { + static func validate(masterPlaylist: any MasterPlaylistInterface) -> [PlaylistValidationIssue] { + var issues = [PlaylistValidationIssue]() + + let issue = duplicateIssue( + tags: masterPlaylist.tags.filter { $0.tagDescriptor == PantosTag.EXT_X_SESSION_DATA } + ) + if let issue { + issues.append(issue) + } + + return 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: [PlaylistTag]) -> PlaylistValidationIssue? { + 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 PlaylistValidationIssue(description: .EXT_X_SESSION_DATAPlaylistValidator, severity: .error) + } + } + return nil + } +} diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift new file mode 100644 index 0000000..3fbecc3 --- /dev/null +++ b/mambaSharedFramework/Pantos-Generic 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: PlaylistTagValidator { + private var genericValidator: GenericDictionaryTagValidator + + init() { + genericValidator = GenericDictionaryTagValidator( + tag: PantosTag.EXT_X_SESSION_DATA, + dictionaryValueIdentifiers: [ + DictionaryTagValueIdentifierImpl( + valueId: PantosValue.dataId, + optional: false, + expectedType: String.self + ), + DictionaryTagValueIdentifierImpl( + valueId: PantosValue.value, + optional: true, + expectedType: String.self + ), + DictionaryTagValueIdentifierImpl( + valueId: PantosValue.uri, + optional: true, + expectedType: String.self + ), + DictionaryTagValueIdentifierImpl( + valueId: PantosValue.format, + optional: true, + expectedType: String.self + ), + DictionaryTagValueIdentifierImpl( + valueId: PantosValue.language, + optional: true, + expectedType: String.self + ), + ] + ) + } + + func validate(tag: PlaylistTag) -> [PlaylistValidationIssue]? { + 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(PlaylistValidationIssue(description: .EXT_X_SESSION_DATATagValidator, severity: .error)) + } + + return issueList.isEmpty ? nil : issueList + } +} diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift new file mode 100644 index 0000000..4404d75 --- /dev/null +++ b/mambaSharedFramework/Pantos-Generic 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: PlaylistTag) -> [PlaylistValidationIssue]? { + var issueList = super.validate(tag: tag) ?? [] + + if let method = tag.value(forValueIdentifier: PantosValue.method) { + if method == EncryptionMethodType.EncryptionMethod.None.rawValue { + issueList.append( + PlaylistValidationIssue( + description: IssueDescription.EXT_X_SESSION_KEYValidator, + severity: IssueSeverity.error + ) + ) + } + } + + return issueList.isEmpty ? nil : issueList + } +} diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/PlaylistValidator.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/PlaylistValidator.swift index 8a1b2bf..72094f1 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/PlaylistValidator.swift +++ b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/PlaylistValidator.swift @@ -65,8 +65,9 @@ public class PlaylistValidator: ExtensiblePlaylistValidator { EXT_X_STREAM_INFRenditionGroupVIDEOValidator.self, EXT_X_STREAM_INFRenditionGroupSUBTITLESValidator.self, PlaylistRenditionGroupMatchingNAMELANGUAGEValidator.self, - PlaylistRenditionGroupMatchingPROGRAM_IDValidator.self] - + PlaylistRenditionGroupMatchingPROGRAM_IDValidator.self, + EXT_X_SESSION_DATAPlaylistValidator.self] + public static let variantPlaylistValidators: [VariantPlaylistValidator.Type] = [PlaylistAggregateTagCardinalityValidator.self, EXT_X_TARGETDURATIONLengthValidator.self, EXT_X_STARTTimeOffsetValidator.self, diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift index 28c6c74..16b44df 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic 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: PlaylistTagDescriptor, 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: PlaylistTagDescriptor, 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: PlaylistTagDescriptor, 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: PlaylistTagDescriptor, 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: PlaylistTagDescriptor, Equatable { DictionaryTagValueIdentifierImpl(valueId: PantosValue.uri, optional: false, expectedType: String.self), DictionaryTagValueIdentifierImpl(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: [ + DictionaryTagValueIdentifierImpl(valueId: PantosValue.method, + optional: false, + expectedType: EncryptionMethodType.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.uri, + optional: false, // URI is REQUIRED since METHOD can't be NONE + expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.ivector, + optional: true, + expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.keyformat, + optional: true, + expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.keyformatVersions, + optional: true, + expectedType: String.self) + ]) + + case .EXT_X_CONTENT_STEERING: + return GenericDictionaryTagValidator(tag: pantostag, dictionaryValueIdentifiers: [ + DictionaryTagValueIdentifierImpl(valueId: PantosValue.serverUri, + optional: false, + expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.pathwayId, + optional: true, + expectedType: String.self) + ]) + case .EXT_X_KEY: return EXT_X_KEYValidator(tag: pantostag, dictionaryValueIdentifiers: [ DictionaryTagValueIdentifierImpl(valueId: PantosValue.method, optional: true, expectedType: EncryptionMethodType.self), @@ -493,6 +552,9 @@ extension PantosTag: PlaylistTagDescriptor, 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 Playlist Parsing/PantosValue.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosValue.swift index 085547e..029fa3b 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosValue.swift +++ b/mambaSharedFramework/Pantos-Generic 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/mambaSharedFramework/PlaylistValidationIssue.swift b/mambaSharedFramework/PlaylistValidationIssue.swift index 4607a6e..e492707 100644 --- a/mambaSharedFramework/PlaylistValidationIssue.swift +++ b/mambaSharedFramework/PlaylistValidationIssue.swift @@ -67,6 +67,9 @@ public enum IssueDescription: String { case PlaylistRenditionGroupMatchingNAMELANGUAGEValidator = "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 PlaylistRenditionGroupMatchingPROGRAM_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_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 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." case EXT_X_STREAM_INFRenditionGroupSUBTITLESValidator = "EXT-X-STREAM-INF - 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 Playlist whose TYPE attribute is SUBTITLES." diff --git a/mambaTests/PantosTagTests.swift b/mambaTests/PantosTagTests.swift index be5450d..4a44948 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 b6ac795..c9bbe1b 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 = createTag(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 = createTag(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 = createTag(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 diff --git a/mambaTests/ValidatorTests.swift b/mambaTests/ValidatorTests.swift index a0462af..8061ebc 100644 --- a/mambaTests/ValidatorTests.swift +++ b/mambaTests/ValidatorTests.swift @@ -152,14 +152,30 @@ class ValidatorTests: XCTestCase { } } - private func validate(validator: MasterPlaylistValidator.Type, playlist: String, expected: Int) { - + @discardableResult + private func validate(validator: MasterPlaylistValidator.Type, playlist: String, expected: Int) -> [PlaylistValidationIssue] { + let playlist = parseMasterPlaylist(inString: playlist) let validationIssues = validator.validate(masterPlaylist: playlist) if validationIssues.count != expected { XCTAssert(false, "Found unexpected validation Issues should have \(expected) actually has \(validationIssues.count)") } + + return validationIssues + } + + private func validate(validator: MasterPlaylistValidator.Type, playlist: String, expectedIssues: [PlaylistValidationIssue]) { + let issues = validate(validator: validator, playlist: playlist, expected: expectedIssues.count) + expectedIssues.forEach { expectedIssue in + guard let matchingIssue = issues.first(where: { $0.description == expectedIssue.description }) else { + return XCTFail("Expected issue \"\(expectedIssue.description)\" not found in multivariant playlist.\nIssues found:\n\(issues)") + } + XCTAssertEqual(expectedIssue.description, matchingIssue.description) + XCTAssertEqual(expectedIssue.severity, + matchingIssue.severity, + "Expected validation issue (\(expectedIssue.description)) had unexpected severity (\(matchingIssue.severity))") + } } func testEXT_X_MEDIARenditionGroupTYPEValidatorOK() { @@ -789,7 +805,33 @@ frag1.ts let expectedIssues = [PlaylistValidationIssue(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: [ + PlaylistValidationIssue(description: .EXT_X_SESSION_DATAPlaylistValidator, severity: .error) + ] + ) + } + } private let masterStreamInf = "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=100,CODECS=\"avc1\",RESOLUTION=10x10\n" From 1b516bea4d6254d1ff170ae196ce5e9f7e09fe62 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 Conflicts: mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift --- .../PantosTag.swift | 6 +++++- .../PantosValue.swift | 14 +++++++++++++- .../GenericDictionaryTagValidatorTests.swift | 13 +++++++++---- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift index 16b44df..e77452e 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift @@ -443,11 +443,15 @@ extension PantosTag: PlaylistTagDescriptor, Equatable { DictionaryTagValueIdentifierImpl(valueId: PantosValue.language, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.assocLanguage, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.name, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.stableRenditionId, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.defaultMedia, optional: true, expectedType: Bool.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.autoselect, optional: true, expectedType: Bool.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.forced, optional: true, expectedType: Bool.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.instreamId, optional: true, expectedType: InstreamId.self), - DictionaryTagValueIdentifierImpl(valueId: PantosValue.characteristics, optional: true, expectedType: String.self) + DictionaryTagValueIdentifierImpl(valueId: PantosValue.bitDepth, optional: true, expectedType: Int.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.sampleRate, optional: true, expectedType: Int.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.characteristics, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.channels, optional: true, expectedType: String.self) ]) case .EXT_X_I_FRAME_STREAM_INF: diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosValue.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosValue.swift index 029fa3b..32684f3 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosValue.swift +++ b/mambaSharedFramework/Pantos-Generic 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 c9bbe1b..05eadc4 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 b638a0e248cbab14e4ada8ad5b699972ba0c5b4d 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 05eadc4..d427bae 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 2acf7186580a08799a6c4697f538bed1d8e6f20a 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 Conflicts: mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift --- .../PantosTag.swift | 12 +- .../PantosValue.swift | 42 +- .../GenericDictionaryTagValidatorTests.swift | 396 +++++++++++++++--- 3 files changed, 381 insertions(+), 69 deletions(-) diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift index e77452e..a19cc4c 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift @@ -426,13 +426,23 @@ extension PantosTag: PlaylistTagDescriptor, Equatable { case .EXT_X_STREAM_INF: return GenericDictionaryTagValidator(tag: pantostag, dictionaryValueIdentifiers: [ DictionaryTagValueIdentifierImpl(valueId: PantosValue.bandwidthBPS, optional: false, expectedType: Int.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.averageBandwidthBPS, optional: true, expectedType: Int.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.score, optional: true, expectedType: Double.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.programId, optional: true, expectedType: Int.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.codecs, optional: true, expectedType: CodecValueTypeArray.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.supplementalCodecs, optional: true, expectedType: CodecValueTypeArray.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.resolution, optional: true, expectedType: ResolutionValueType.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.frameRate, optional: true, expectedType: Double.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.stableVariantId, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.audioGroup, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.videoGroup, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.subtitlesGroup, optional: true, expectedType: String.self), - DictionaryTagValueIdentifierImpl(valueId: PantosValue.closedCaptionsGroup, optional: true, expectedType: ClosedCaptionsValueType.self) + DictionaryTagValueIdentifierImpl(valueId: PantosValue.closedCaptionsGroup, optional: true, expectedType: ClosedCaptionsValueType.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.pathwayId, optional: true, expectedType: String.self) ]) case .EXT_X_MEDIA: diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosValue.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosValue.swift index 32684f3..d69bc96 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosValue.swift +++ b/mambaSharedFramework/Pantos-Generic 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 d427bae..d5235c7 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 880729d1da376e7db91d4b3f71a4553178d70350 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 Conflicts: mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift --- .../PantosTag.swift | 13 +++- .../GenericDictionaryTagValidatorTests.swift | 64 +++++++++++-------- 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift index a19cc4c..473bf00 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift @@ -466,12 +466,21 @@ extension PantosTag: PlaylistTagDescriptor, Equatable { case .EXT_X_I_FRAME_STREAM_INF: return GenericDictionaryTagValidator(tag: pantostag, dictionaryValueIdentifiers: [ + DictionaryTagValueIdentifierImpl(valueId: PantosValue.uri, optional: false, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.bandwidthBPS, optional: false, expectedType: Int.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.averageBandwidthBPS, optional: true, expectedType: Int.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.score, optional: true, expectedType: Double.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.programId, optional: true, expectedType: Int.self), - DictionaryTagValueIdentifierImpl(valueId: PantosValue.codecs, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.codecs, optional: true, expectedType: CodecValueTypeArray.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.supplementalCodecs, optional: true, expectedType: CodecValueTypeArray.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.resolution, optional: true, expectedType: ResolutionValueType.self), - DictionaryTagValueIdentifierImpl(valueId: PantosValue.uri, optional: false, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.stableVariantId, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.videoGroup, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(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 d5235c7..d19e2c0 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 4d7f23d6e54a1a0cc75b6142b5d2abad6b3bf35f 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/ValueTypes.swift | 1 + .../GenericDictionaryTagValidatorTests.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mambaSharedFramework/ValueTypes.swift b/mambaSharedFramework/ValueTypes.swift index 3d4a273..7bd1395 100644 --- a/mambaSharedFramework/ValueTypes.swift +++ b/mambaSharedFramework/ValueTypes.swift @@ -127,6 +127,7 @@ public struct EncryptionMethodType: Equatable, FailableStringLiteralConvertible case None = "NONE" case AES128 = "AES-128" case SampleAES = "SAMPLE-AES" + case SampleAESCTR = "SAMPLE-AES-CTR" } public init?(failableInitWithString string: String) { self.init(encryption: string) diff --git a/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index d19e2c0..6f4cfa8 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 d50ba53235b2298729064fb5b3d915c01e2e8f7e 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 Conflicts: mamba.xcodeproj/project.pbxproj mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift --- mamba.xcodeproj/project.pbxproj | 10 +- .../PantosTag.swift | 2 +- mambaSharedFramework/ValueTypes.swift | 103 ++++++++++++++ .../GenericDictionaryTagValidatorTests.swift | 3 +- .../Value Types/ChannelsTests.swift | 134 ++++++++++++++++++ 5 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 mambaTests/Util Tests/Value Types/ChannelsTests.swift diff --git a/mamba.xcodeproj/project.pbxproj b/mamba.xcodeproj/project.pbxproj index cd33869..19bd942 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 /* ChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758382C8620C000D12CCD /* ChannelsTests.swift */; }; + 1447583A2C8620C000D12CCD /* ChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758382C8620C000D12CCD /* ChannelsTests.swift */; }; + 1447583B2C8620C000D12CCD /* ChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758382C8620C000D12CCD /* ChannelsTests.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 */; }; @@ -647,6 +650,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 /* ChannelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsTests.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 = ""; }; @@ -1333,10 +1337,11 @@ EC9BCAA21D749D8B0032BEBE /* Value Types */ = { isa = PBXGroup; children = ( + 144758382C8620C000D12CCD /* ChannelsTests.swift */, EC7492AF1DD29F8900AF4E20 /* CodecArrayTests.swift */, EC7492B01DD29F8900AF4E20 /* MediaTypeTests.swift */, - EC7492B21DD29F8900AF4E20 /* ResolutionTests.swift */, EC7492B11DD29F8900AF4E20 /* PlaylistTypeTests.swift */, + EC7492B21DD29F8900AF4E20 /* ResolutionTests.swift */, ); path = "Value Types"; sourceTree = ""; @@ -1948,6 +1953,7 @@ ECFBD9101E5CCC2200379FC2 /* ParseArrayTests.m in Sources */, EC7492B31DD29F8900AF4E20 /* CodecArrayTests.swift in Sources */, 01CD2E7A1DE4D46F002510E7 /* EXT_X_MAPTagParserTests.swift in Sources */, + 144758392C8620C000D12CCD /* ChannelsTests.swift in Sources */, EC7492AB1DD29F7000AF4E20 /* OrderedDictionaryTests.swift in Sources */, EC7492781DD29EC800AF4E20 /* EXT_X_MEDIATagParserTests.swift in Sources */, ); @@ -2123,6 +2129,7 @@ ECFBD9111E5CCC2200379FC2 /* ParseArrayTests.m in Sources */, EC7492B81DD29F8900AF4E20 /* PlaylistTypeTests.swift in Sources */, EC7492B41DD29F8900AF4E20 /* CodecArrayTests.swift in Sources */, + 1447583A2C8620C000D12CCD /* ChannelsTests.swift in Sources */, EC7492AC1DD29F7000AF4E20 /* OrderedDictionaryTests.swift in Sources */, EC7492791DD29EC800AF4E20 /* EXT_X_MEDIATagParserTests.swift in Sources */, ); @@ -2296,6 +2303,7 @@ ECE253FD209A50B500D388CE /* ThirdPartyTagListSupportTests.swift in Sources */, ECE25408209A50B500D388CE /* ResolutionTests.swift in Sources */, ECE253F6209A50B500D388CE /* GenericSingleValueTagParserTests.swift in Sources */, + 1447583B2C8620C000D12CCD /* ChannelsTests.swift in Sources */, ECE253FA209A50B500D388CE /* GenericSingleTagValidatorTests.swift in Sources */, ECE253F1209A50B500D388CE /* EXT_X_KEYTagParserTests.swift in Sources */, ECE25403209A50B500D388CE /* String+Helio.swift in Sources */, diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift index 473bf00..50e49b4 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift @@ -461,7 +461,7 @@ extension PantosTag: PlaylistTagDescriptor, Equatable { DictionaryTagValueIdentifierImpl(valueId: PantosValue.bitDepth, optional: true, expectedType: Int.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.sampleRate, optional: true, expectedType: Int.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.characteristics, optional: true, expectedType: String.self), - DictionaryTagValueIdentifierImpl(valueId: PantosValue.channels, optional: true, expectedType: String.self) + DictionaryTagValueIdentifierImpl(valueId: PantosValue.channels, optional: true, expectedType: Channels.self) ]) case .EXT_X_I_FRAME_STREAM_INF: diff --git a/mambaSharedFramework/ValueTypes.swift b/mambaSharedFramework/ValueTypes.swift index 7bd1395..b83a744 100644 --- a/mambaSharedFramework/ValueTypes.swift +++ b/mambaSharedFramework/ValueTypes.swift @@ -206,6 +206,109 @@ public struct ClosedCaptionsValueType: FailableStringLiteralConvertible { } } +/// Represents CHANNELS +public struct Channels: 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?(failableInitWithString 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(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/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index 6f4cfa8..a6aa751 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/ChannelsTests.swift b/mambaTests/Util Tests/Value Types/ChannelsTests.swift new file mode 100644 index 0000000..b8d1ad4 --- /dev/null +++ b/mambaTests/Util Tests/Value Types/ChannelsTests.swift @@ -0,0 +1,134 @@ +// +// ChannelsTests.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 ChannelsTests: 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 = Channels(failableInitWithString: empty) + XCTAssertNil(actualChannels) + } + + func test_invalidCount() { + let actualChannels = Channels(failableInitWithString: invalidCount) + XCTAssertNil(actualChannels) + } + + func test_sixChannel() { + let actualChannels = Channels(failableInitWithString: sixChannel) + let expectedChannels = Channels( + count: 6, + spatialAudioCodingIdentifiers: [], + specialUsageIdentifiers: [] + ) + XCTAssertEqual(expectedChannels, actualChannels) + } + + func test_twelveChannelJoc() { + let actualChannels = Channels(failableInitWithString: twelveChannelJoc) + let expectedChannels = Channels( + count: 12, + spatialAudioCodingIdentifiers: ["JOC"], + specialUsageIdentifiers: [] + ) + XCTAssertEqual(expectedChannels, actualChannels) + } + + func test_twelveChannelJocAndUnknownSpatialCoding() { + let actualChannels = Channels(failableInitWithString: twelveChannelJocAndUnknownSpatialCoding) + let expectedChannels = Channels( + count: 12, + spatialAudioCodingIdentifiers: ["JOC", "SPECIAL"], + specialUsageIdentifiers: [] + ) + XCTAssertEqual(expectedChannels, actualChannels) + } + + func test_sixChannelWithEmptySpatialIdentifier() { + let actualChannels = Channels(failableInitWithString: sixChannelWithEmptySpatialIdentifier) + let expectedChannels = Channels( + count: 6, + spatialAudioCodingIdentifiers: [], + specialUsageIdentifiers: [] + ) + XCTAssertEqual(expectedChannels, actualChannels) + } + + func test_twelveChannelUnknownSpatialWithDashInName() { + let actualChannels = Channels(failableInitWithString: twelveChannelUnknownSpatialWithDashInName) + let expectedChannels = Channels( + count: 12, + spatialAudioCodingIdentifiers: ["VERY-SPATIAL"], + specialUsageIdentifiers: [] + ) + XCTAssertEqual(expectedChannels, actualChannels) + } + + func test_sixChannelNoSpatialWithDownmix() { + let actualChannels = Channels(failableInitWithString: sixChannelNoSpatialWithDownmix) + let expectedChannels = Channels( + count: 6, + spatialAudioCodingIdentifiers: [], + specialUsageIdentifiers: [.downmix] + ) + XCTAssertEqual(expectedChannels, actualChannels) + } + + func test_sixChannelNoSpatialWithBinauralAndImmersive() { + let actualChannels = Channels(failableInitWithString: sixChannelNoSpatialWithBinauralAndImmersive) + let expectedChannels = Channels( + count: 6, + spatialAudioCodingIdentifiers: [], + specialUsageIdentifiers: [.binaural, .immersive] + ) + XCTAssertEqual(expectedChannels, actualChannels) + } + + func test_twelveChannelJocAndImmersive() { + let actualChannels = Channels(failableInitWithString: twelveChannelJocAndImmersive) + let expectedChannels = Channels( + 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 = Channels(failableInitWithString: sixChannelUnknownSpecialUsageIdentifier) + XCTAssertNil(actualChannels) + } +} From b662467e69a45abdd9e48d48cbe5991913d71e1b 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 Conflicts: mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift mambaSharedFramework/ValueTypes.swift --- .../PantosTag.swift | 4 +-- mambaSharedFramework/ValueTypes.swift | 29 ++++++++++++++++++- .../GenericDictionaryTagValidatorTests.swift | 6 ++-- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift index 50e49b4..e09ebec 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift @@ -433,7 +433,7 @@ extension PantosTag: PlaylistTagDescriptor, Equatable { DictionaryTagValueIdentifierImpl(valueId: PantosValue.supplementalCodecs, optional: true, expectedType: CodecValueTypeArray.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.resolution, optional: true, expectedType: ResolutionValueType.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.frameRate, optional: true, expectedType: Double.self), - DictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: HDCPLevel.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: String.self), @@ -474,7 +474,7 @@ extension PantosTag: PlaylistTagDescriptor, Equatable { DictionaryTagValueIdentifierImpl(valueId: PantosValue.codecs, optional: true, expectedType: CodecValueTypeArray.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.supplementalCodecs, optional: true, expectedType: CodecValueTypeArray.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.resolution, optional: true, expectedType: ResolutionValueType.self), - DictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: HDCPLevel.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: String.self), diff --git a/mambaSharedFramework/ValueTypes.swift b/mambaSharedFramework/ValueTypes.swift index b83a744..26cc7bf 100644 --- a/mambaSharedFramework/ValueTypes.swift +++ b/mambaSharedFramework/ValueTypes.swift @@ -120,7 +120,7 @@ public func !=(lhs: MediaType.Media, rhs: MediaType) -> 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 EncryptionMethodType: Equatable, FailableStringLiteralConvertible { public let type: EncryptionMethod public enum EncryptionMethod: String { @@ -147,6 +147,33 @@ public func ==(lhs: EncryptionMethodType, rhs: EncryptionMethodType) -> Bool { return lhs.type == rhs.type } +/// Represents a minimum required HDCP level needed to play content. +public struct HDCPLevel: 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?(failableInitWithString 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/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index a6aa751..376885b 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 f7c2e575ddbbd666f282afa2e3b859b9b7b86b91 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 Conflicts: mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift --- .../PantosTag.swift | 4 +- mambaSharedFramework/ValueTypes.swift | 38 +++++++++++++++++++ .../GenericDictionaryTagValidatorTests.swift | 6 ++- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift index e09ebec..f34bc31 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift @@ -435,7 +435,7 @@ extension PantosTag: PlaylistTagDescriptor, Equatable { DictionaryTagValueIdentifierImpl(valueId: PantosValue.frameRate, optional: true, expectedType: Double.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: HDCPLevel.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self), - DictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: VideoRange.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.stableVariantId, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.audioGroup, optional: true, expectedType: String.self), @@ -476,7 +476,7 @@ extension PantosTag: PlaylistTagDescriptor, Equatable { DictionaryTagValueIdentifierImpl(valueId: PantosValue.resolution, optional: true, expectedType: ResolutionValueType.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: HDCPLevel.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self), - DictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: VideoRange.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.stableVariantId, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.videoGroup, optional: true, expectedType: String.self), diff --git a/mambaSharedFramework/ValueTypes.swift b/mambaSharedFramework/ValueTypes.swift index 26cc7bf..2deb32f 100644 --- a/mambaSharedFramework/ValueTypes.swift +++ b/mambaSharedFramework/ValueTypes.swift @@ -174,6 +174,44 @@ public struct HDCPLevel: 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 VideoRange: 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?(failableInitWithString 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/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index 376885b..946a0fe 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 f48196c5bc33c15a2dddffb9a476511bf9c89bfa 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 Conflicts: mamba.xcodeproj/project.pbxproj mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift --- mamba.xcodeproj/project.pbxproj | 8 ++ .../PantosTag.swift | 4 +- mambaSharedFramework/ValueTypes.swift | 64 ++++++++++++++ .../GenericDictionaryTagValidatorTests.swift | 6 +- .../Value Types/VideoLayoutTests.swift | 84 +++++++++++++++++++ 5 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 mambaTests/Util Tests/Value Types/VideoLayoutTests.swift diff --git a/mamba.xcodeproj/project.pbxproj b/mamba.xcodeproj/project.pbxproj index 19bd942..6c760ac 100644 --- a/mamba.xcodeproj/project.pbxproj +++ b/mamba.xcodeproj/project.pbxproj @@ -20,6 +20,9 @@ 144758392C8620C000D12CCD /* ChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758382C8620C000D12CCD /* ChannelsTests.swift */; }; 1447583A2C8620C000D12CCD /* ChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758382C8620C000D12CCD /* ChannelsTests.swift */; }; 1447583B2C8620C000D12CCD /* ChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758382C8620C000D12CCD /* ChannelsTests.swift */; }; + 1447583D2C8693E000D12CCD /* VideoLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1447583C2C8693E000D12CCD /* VideoLayoutTests.swift */; }; + 1447583E2C8693E000D12CCD /* VideoLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1447583C2C8693E000D12CCD /* VideoLayoutTests.swift */; }; + 1447583F2C8693E000D12CCD /* VideoLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1447583C2C8693E000D12CCD /* VideoLayoutTests.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 */; }; @@ -651,6 +654,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 /* ChannelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsTests.swift; sourceTree = ""; }; + 1447583C2C8693E000D12CCD /* VideoLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoLayoutTests.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 = ""; }; @@ -1342,6 +1346,7 @@ EC7492B01DD29F8900AF4E20 /* MediaTypeTests.swift */, EC7492B11DD29F8900AF4E20 /* PlaylistTypeTests.swift */, EC7492B21DD29F8900AF4E20 /* ResolutionTests.swift */, + 1447583C2C8693E000D12CCD /* VideoLayoutTests.swift */, ); path = "Value Types"; sourceTree = ""; @@ -1956,6 +1961,7 @@ 144758392C8620C000D12CCD /* ChannelsTests.swift in Sources */, EC7492AB1DD29F7000AF4E20 /* OrderedDictionaryTests.swift in Sources */, EC7492781DD29EC800AF4E20 /* EXT_X_MEDIATagParserTests.swift in Sources */, + 1447583D2C8693E000D12CCD /* VideoLayoutTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2132,6 +2138,7 @@ 1447583A2C8620C000D12CCD /* ChannelsTests.swift in Sources */, EC7492AC1DD29F7000AF4E20 /* OrderedDictionaryTests.swift in Sources */, EC7492791DD29EC800AF4E20 /* EXT_X_MEDIATagParserTests.swift in Sources */, + 1447583E2C8693E000D12CCD /* VideoLayoutTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2308,6 +2315,7 @@ ECE253F1209A50B500D388CE /* EXT_X_KEYTagParserTests.swift in Sources */, ECE25403209A50B500D388CE /* String+Helio.swift in Sources */, ECE25400209A50B500D388CE /* IndeterminateBoolTests.swift in Sources */, + 1447583F2C8693E000D12CCD /* VideoLayoutTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift index f34bc31..cc2a539 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift @@ -436,7 +436,7 @@ extension PantosTag: PlaylistTagDescriptor, Equatable { DictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: HDCPLevel.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: VideoRange.self), - DictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: VideoLayout.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.stableVariantId, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.audioGroup, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.videoGroup, optional: true, expectedType: String.self), @@ -477,7 +477,7 @@ extension PantosTag: PlaylistTagDescriptor, Equatable { DictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: HDCPLevel.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: VideoRange.self), - DictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: VideoLayout.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.stableVariantId, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.videoGroup, optional: true, expectedType: String.self), DictionaryTagValueIdentifierImpl(valueId: PantosValue.pathwayId, optional: true, expectedType: String.self), diff --git a/mambaSharedFramework/ValueTypes.swift b/mambaSharedFramework/ValueTypes.swift index 2deb32f..28bcb77 100644 --- a/mambaSharedFramework/ValueTypes.swift +++ b/mambaSharedFramework/ValueTypes.swift @@ -451,4 +451,68 @@ public func ==(lhs: CodecValueTypeArray, rhs: CodecValueTypeArray) -> 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 VideoLayout: 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?(failableInitWithString 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/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index 946a0fe..f468590 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/VideoLayoutTests.swift b/mambaTests/Util Tests/Value Types/VideoLayoutTests.swift new file mode 100644 index 0000000..0415971 --- /dev/null +++ b/mambaTests/Util Tests/Value Types/VideoLayoutTests.swift @@ -0,0 +1,84 @@ +// +// VideoLayoutTests.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 VideoLayoutTests: 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(VideoLayout(failableInitWithString: empty)) + } + + func test_invalidVideoLayout() { + XCTAssertNil(VideoLayout(failableInitWithString: invalidVideoLayout)) + } + + func test_monoLayout() { + guard let videoLayout = VideoLayout(failableInitWithString: 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 = VideoLayout(failableInitWithString: 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 = VideoLayout(failableInitWithString: 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 = VideoLayout(failableInitWithString: 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(VideoLayout(failableInitWithString: monoWithStereoWithUnknownLayout)) + } +} From 23ded16f062e56e0c3f40c3a5ae8ba8d592cf0d1 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 Conflicts: mambaSharedFramework/ValueTypes.swift --- .../EXT_X_SESSION_DATATagValidator.swift | 2 +- mambaSharedFramework/ValueTypes.swift | 23 +++++++++++++++++-- .../GenericDictionaryTagValidatorTests.swift | 6 ++--- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift index 3fbecc3..5cc8ed0 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift +++ b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift @@ -44,7 +44,7 @@ class EXT_X_SESSION_DATATagValidator: PlaylistTagValidator { DictionaryTagValueIdentifierImpl( valueId: PantosValue.format, optional: true, - expectedType: String.self + expectedType: SessionDataFormat.self ), DictionaryTagValueIdentifierImpl( valueId: PantosValue.language, diff --git a/mambaSharedFramework/ValueTypes.swift b/mambaSharedFramework/ValueTypes.swift index 28bcb77..86e5e8f 100644 --- a/mambaSharedFramework/ValueTypes.swift +++ b/mambaSharedFramework/ValueTypes.swift @@ -212,6 +212,27 @@ public struct VideoRange: Equatable, FailableStringLiteralConvertible { } } +/// Represents the format of the file referenced by `EXT-X-SESSION-DATA:URI`. +public struct SessionDataFormat: Equatable, FailableStringLiteralConvertible { + public let type: Format + public enum Format: String { + case json = "JSON" + case raw = "RAW" + } + public init?(failableInitWithString 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 @@ -242,7 +263,6 @@ public func ==(lhs: PlaylistValueType, rhs: PlaylistValueType) -> Bool { /// Represents a instreamId type /// /// Can be initialized with a string "CC1" or "CC2" or "CC3" or "CC4" for a valid value - public enum InstreamId: String, FailableStringLiteralConvertible { case CC1 = "CC1" case CC2 = "CC2" @@ -255,7 +275,6 @@ public enum InstreamId: 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/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift b/mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift index f468590..df97508 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 3ea53bbb063289a87f8c78a44ef8e91583862cc4 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 Conflicts: mambaSharedFramework/Pantos-Generic HLS Playlist Parsing/Pantos-Generic Tag Validators/HLSPlaylistValidatorImpl.swift --- mambaTests/ValidatorTests.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/mambaTests/ValidatorTests.swift b/mambaTests/ValidatorTests.swift index 8061ebc..23eab83 100644 --- a/mambaTests/ValidatorTests.swift +++ b/mambaTests/ValidatorTests.swift @@ -832,6 +832,17 @@ frag1.ts ) } + func testEXT_X_SESSION_DATAPlaylistValidator_existsWithinMasterPlaylistValidators() { + XCTAssertEqual( + 1, + PlaylistValidator.masterPlaylistValidators.filter { $0 == EXT_X_SESSION_DATAPlaylistValidator.self }.count + ) + XCTAssertEqual( + 0, + PlaylistValidator.variantPlaylistValidators.filter { $0 == EXT_X_SESSION_DATAPlaylistValidator.self }.count + ) + } + } private let masterStreamInf = "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=100,CODECS=\"avc1\",RESOLUTION=10x10\n" From 1240a1b8f1a3a0dc50c5c38c141dc674e3f2119e 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 Conflicts: mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift --- .../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 Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift index 5cc8ed0..f6c0fbc 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift +++ b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATATagValidator.swift @@ -19,7 +19,7 @@ import Foundation -class EXT_X_SESSION_DATATagValidator: PlaylistTagValidator { +struct EXT_X_SESSION_DATATagValidator: PlaylistTagValidator { private var genericValidator: GenericDictionaryTagValidator init() { diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift index 4404d75..7f16294 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift +++ b/mambaSharedFramework/Pantos-Generic 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: PlaylistTagValidator { + private let keyValidator: EXT_X_KEYValidator - override public func validate(tag: PlaylistTag) -> [PlaylistValidationIssue]? { - var issueList = super.validate(tag: tag) ?? [] + init() { + keyValidator = EXT_X_KEYValidator(tag: PantosTag.EXT_X_SESSION_KEY, dictionaryValueIdentifiers: [ + DictionaryTagValueIdentifierImpl(valueId: PantosValue.method, + optional: false, + expectedType: EncryptionMethodType.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.uri, + optional: false, // URI is REQUIRED since METHOD can't be NONE + expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.ivector, + optional: true, + expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.keyformat, + optional: true, + expectedType: String.self), + DictionaryTagValueIdentifierImpl(valueId: PantosValue.keyformatVersions, + optional: true, + expectedType: String.self) + ]) + } + + public func validate(tag: PlaylistTag) -> [PlaylistValidationIssue]? { + var issueList = keyValidator.validate(tag: tag) ?? [] if let method = tag.value(forValueIdentifier: PantosValue.method) { if method == EncryptionMethodType.EncryptionMethod.None.rawValue { diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift index cc2a539..e83f30f 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift +++ b/mambaSharedFramework/Pantos-Generic Playlist Parsing/PantosTag.swift @@ -487,23 +487,7 @@ extension PantosTag: PlaylistTagDescriptor, Equatable { return EXT_X_SESSION_DATATagValidator() case .EXT_X_SESSION_KEY: - return EXT_X_SESSION_KEYValidator(tag: pantostag, dictionaryValueIdentifiers: [ - DictionaryTagValueIdentifierImpl(valueId: PantosValue.method, - optional: false, - expectedType: EncryptionMethodType.self), - DictionaryTagValueIdentifierImpl(valueId: PantosValue.uri, - optional: false, // URI is REQUIRED since METHOD can't be NONE - expectedType: String.self), - DictionaryTagValueIdentifierImpl(valueId: PantosValue.ivector, - optional: true, - expectedType: String.self), - DictionaryTagValueIdentifierImpl(valueId: PantosValue.keyformat, - optional: true, - expectedType: String.self), - DictionaryTagValueIdentifierImpl(valueId: PantosValue.keyformatVersions, - optional: true, - expectedType: String.self) - ]) + return EXT_X_SESSION_KEYValidator() case .EXT_X_CONTENT_STEERING: return GenericDictionaryTagValidator(tag: pantostag, dictionaryValueIdentifiers: [ From 692ee64a85e847f4c66a4dc943656f97b9887ed2 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 Conflicts: mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift --- .../EXT_X_SESSION_DATAPlaylistValidator.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift index c687f6f..cf66b36 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift +++ b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift @@ -23,10 +23,9 @@ class EXT_X_SESSION_DATAPlaylistValidator: MasterPlaylistValidator { static func validate(masterPlaylist: any MasterPlaylistInterface) -> [PlaylistValidationIssue] { var issues = [PlaylistValidationIssue]() - let issue = duplicateIssue( + if let issue = duplicateIssue( tags: masterPlaylist.tags.filter { $0.tagDescriptor == PantosTag.EXT_X_SESSION_DATA } - ) - if let issue { + ) { issues.append(issue) } From d064651e60ef783ac84ab49c87da2ea212c47741 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 Conflicts: mambaSharedFramework/ValueTypes.swift --- mambaSharedFramework/ValueTypes.swift | 20 ++++++++----------- .../Value Types/VideoLayoutTests.swift | 16 +++++++-------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/mambaSharedFramework/ValueTypes.swift b/mambaSharedFramework/ValueTypes.swift index 86e5e8f..cb3fb31 100644 --- a/mambaSharedFramework/ValueTypes.swift +++ b/mambaSharedFramework/ValueTypes.swift @@ -476,15 +476,15 @@ public func ==(lhs: CodecValueTypeArray, rhs: CodecValueTypeArray) -> Bool { public struct VideoLayout: 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. @@ -504,9 +504,9 @@ public struct VideoLayout: Equatable, FailableStringLiteralConvertible { } public init?(failableInitWithString 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- @@ -521,17 +521,13 @@ public struct VideoLayout: 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/VideoLayoutTests.swift b/mambaTests/Util Tests/Value Types/VideoLayoutTests.swift index 0415971..2e419f3 100644 --- a/mambaTests/Util Tests/Value Types/VideoLayoutTests.swift +++ b/mambaTests/Util Tests/Value Types/VideoLayoutTests.swift @@ -44,8 +44,8 @@ class VideoLayoutTests: 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 VideoLayoutTests: 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 VideoLayoutTests: 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 VideoLayoutTests: 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 93dfacf0df8b3a9ecbca48605c4b5ff09387d111 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 Conflicts: mambaSharedFramework/ValueTypes.swift --- mambaSharedFramework/ValueTypes.swift | 95 +++++++++------------------ 1 file changed, 31 insertions(+), 64 deletions(-) diff --git a/mambaSharedFramework/ValueTypes.swift b/mambaSharedFramework/ValueTypes.swift index cb3fb31..04395b5 100644 --- a/mambaSharedFramework/ValueTypes.swift +++ b/mambaSharedFramework/ValueTypes.swift @@ -148,29 +148,18 @@ public func ==(lhs: EncryptionMethodType, rhs: EncryptionMethodType) -> Bool { } /// Represents a minimum required HDCP level needed to play content. -public struct HDCPLevel: 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 HDCPLevel: 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?(failableInitWithString 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) } } @@ -182,54 +171,32 @@ public struct HDCPLevel: 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 VideoRange: 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 VideoRange: 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?(failableInitWithString 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 SessionDataFormat: Equatable, FailableStringLiteralConvertible { - public let type: Format - public enum Format: String { - case json = "JSON" - case raw = "RAW" - } +public enum SessionDataFormat: String, Equatable, FailableStringLiteralConvertible { + case json = "JSON" + case raw = "RAW" + public init?(failableInitWithString 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 2782f2da6e84544ed386277fcb9ea8bc9a709322 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 Conflicts: mambaSharedFramework/ValueTypes.swift mambaTests/Util Tests/Value Types/ChannelsTests.swift mambaTests/Util Tests/Value Types/VideoLayoutTests.swift --- mambaSharedFramework/ValueTypes.swift | 86 ++++++++++--------- .../GenericDictionaryTagValidatorTests.swift | 6 +- .../Value Types/ChannelsTests.swift | 10 ++- .../Value Types/VideoLayoutTests.swift | 26 ++++-- 4 files changed, 74 insertions(+), 54 deletions(-) diff --git a/mambaSharedFramework/ValueTypes.swift b/mambaSharedFramework/ValueTypes.swift index 04395b5..4132c22 100644 --- a/mambaSharedFramework/ValueTypes.swift +++ b/mambaSharedFramework/ValueTypes.swift @@ -271,25 +271,41 @@ public struct Channels: 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)) } } } @@ -303,15 +319,7 @@ public struct Channels: 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. } } @@ -347,16 +355,8 @@ public struct Channels: 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) } } } @@ -451,36 +451,42 @@ public struct VideoLayout: 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?(failableInitWithString 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 df97508..7bcf38d 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/ChannelsTests.swift b/mambaTests/Util Tests/Value Types/ChannelsTests.swift index b8d1ad4..b2dd533 100644 --- a/mambaTests/Util Tests/Value Types/ChannelsTests.swift +++ b/mambaTests/Util Tests/Value Types/ChannelsTests.swift @@ -124,11 +124,13 @@ class ChannelsTests: 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 = Channels(failableInitWithString: sixChannelUnknownSpecialUsageIdentifier) - XCTAssertNil(actualChannels) + let expectedChannels = Channels( + count: 6, + spatialAudioCodingIdentifiers: [], + specialUsageIdentifiers: [.unrecognized("NEW-IDENTIFIER")] + ) + XCTAssertEqual(expectedChannels, actualChannels) } } diff --git a/mambaTests/Util Tests/Value Types/VideoLayoutTests.swift b/mambaTests/Util Tests/Value Types/VideoLayoutTests.swift index 2e419f3..1a0ebb8 100644 --- a/mambaTests/Util Tests/Value Types/VideoLayoutTests.swift +++ b/mambaTests/Util Tests/Value Types/VideoLayoutTests.swift @@ -23,19 +23,26 @@ import mamba class VideoLayoutTests: 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(VideoLayout(failableInitWithString: empty)) } - func test_invalidVideoLayout() { - XCTAssertNil(VideoLayout(failableInitWithString: invalidVideoLayout)) + func test_unrecognizedVideoLayout() { + guard let videoLayout = VideoLayout(failableInitWithString: 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 VideoLayoutTests: XCTestCase { XCTAssertTrue(videoLayout.contains(.chStereo)) } - func test_monoWithStereoWithUnknownLayout() { - XCTAssertNil(VideoLayout(failableInitWithString: monoWithStereoWithUnknownLayout)) + func test_monoWithStereoWithUnrecognizedLayout() { + guard let videoLayout = VideoLayout(failableInitWithString: 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 576c6c8d3e67b1e7d4509e68f01b2a5dcf8e52f7 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 Conflicts: mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift --- .../EXT_X_SESSION_DATAPlaylistValidator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift index cf66b36..a649287 100644 --- a/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift +++ b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift @@ -19,7 +19,7 @@ import Foundation -class EXT_X_SESSION_DATAPlaylistValidator: MasterPlaylistValidator { +final class EXT_X_SESSION_DATAPlaylistValidator: MasterPlaylistValidator { static func validate(masterPlaylist: any MasterPlaylistInterface) -> [PlaylistValidationIssue] { var issues = [PlaylistValidationIssue]()