diff --git a/mamba.xcodeproj/project.pbxproj b/mamba.xcodeproj/project.pbxproj index 5a182e5..6c760ac 100644 --- a/mamba.xcodeproj/project.pbxproj +++ b/mamba.xcodeproj/project.pbxproj @@ -8,6 +8,21 @@ /* 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 */; }; + 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 */; }; @@ -635,6 +650,11 @@ /* 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 = ""; }; + 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 = ""; }; @@ -930,6 +950,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 */, @@ -1318,10 +1341,12 @@ EC9BCAA21D749D8B0032BEBE /* Value Types */ = { isa = PBXGroup; children = ( + 144758382C8620C000D12CCD /* ChannelsTests.swift */, EC7492AF1DD29F8900AF4E20 /* CodecArrayTests.swift */, EC7492B01DD29F8900AF4E20 /* MediaTypeTests.swift */, - EC7492B21DD29F8900AF4E20 /* ResolutionTests.swift */, EC7492B11DD29F8900AF4E20 /* PlaylistTypeTests.swift */, + EC7492B21DD29F8900AF4E20 /* ResolutionTests.swift */, + 1447583C2C8693E000D12CCD /* VideoLayoutTests.swift */, ); path = "Value Types"; sourceTree = ""; @@ -1773,6 +1798,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 +1873,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 */, @@ -1930,8 +1958,10 @@ 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 */, + 1447583D2C8693E000D12CCD /* VideoLayoutTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1945,6 +1975,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 +2050,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 */, @@ -2102,8 +2135,10 @@ 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 */, + 1447583E2C8693E000D12CCD /* VideoLayoutTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2118,6 +2153,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 +2225,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 */, @@ -2272,10 +2310,12 @@ 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 */, ECE25400209A50B500D388CE /* IndeterminateBoolTests.swift in Sources */, + 1447583F2C8693E000D12CCD /* VideoLayoutTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 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..a649287 --- /dev/null +++ b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_DATAPlaylistValidator.swift @@ -0,0 +1,52 @@ +// +// 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 + +final class EXT_X_SESSION_DATAPlaylistValidator: MasterPlaylistValidator { + static func validate(masterPlaylist: any MasterPlaylistInterface) -> [PlaylistValidationIssue] { + var issues = [PlaylistValidationIssue]() + + if let issue = duplicateIssue( + tags: masterPlaylist.tags.filter { $0.tagDescriptor == PantosTag.EXT_X_SESSION_DATA } + ) { + 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..f6c0fbc --- /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 + +struct 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: SessionDataFormat.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..7f16294 --- /dev/null +++ b/mambaSharedFramework/Pantos-Generic Playlist Parsing/Pantos-Generic Tag Validators/EXT_X_SESSION_KEYValidator.swift @@ -0,0 +1,63 @@ +// +// 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. +struct EXT_X_SESSION_KEYValidator: PlaylistTagValidator { + private let keyValidator: EXT_X_KEYValidator + + 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 { + 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 4278967..8c8b8ae 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" @@ -129,6 +132,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: @@ -196,6 +205,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: @@ -276,6 +291,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: @@ -342,6 +363,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: @@ -408,13 +435,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: 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: 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), 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: @@ -425,23 +462,52 @@ 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: Channels.self) ]) 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: 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: 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), ]) - + + case .EXT_X_SESSION_DATA: + return EXT_X_SESSION_DATATagValidator() + + case .EXT_X_SESSION_KEY: + return EXT_X_SESSION_KEYValidator() + + 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), @@ -512,6 +578,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 a6d7c8a..9a4ffaf 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,16 +54,57 @@ 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" /// 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_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. case targetDurationSeconds = "targetDurationSeconds" @@ -79,7 +126,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 @@ -99,8 +146,20 @@ 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`. 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" /// 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/mambaSharedFramework/ValueTypes.swift b/mambaSharedFramework/ValueTypes.swift index 3d4a273..4132c22 100644 --- a/mambaSharedFramework/ValueTypes.swift +++ b/mambaSharedFramework/ValueTypes.swift @@ -120,13 +120,14 @@ 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 { 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) @@ -146,6 +147,59 @@ public func ==(lhs: EncryptionMethodType, rhs: EncryptionMethodType) -> Bool { return lhs.type == rhs.type } +/// Represents a minimum required HDCP level needed to play content. +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(rawValue: string) + } +} + +/// 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 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(rawValue: string) + } +} + +/// Represents the format of the file referenced by `EXT-X-SESSION-DATA:URI`. +public enum SessionDataFormat: String, Equatable, FailableStringLiteralConvertible { + case json = "JSON" + case raw = "RAW" + + public init?(failableInitWithString string: String) { + self.init(rawValue: string) + } +} + /// Represents a playlist type /// /// Can be initialized with a string "EVENT" or "VOD" for a valid value @@ -176,7 +230,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" @@ -189,7 +242,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 @@ -205,6 +257,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: 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 + /// The audio is pre-processed content that SHOULD NOT be dynamically spatialized. It is suitable to deliver to + /// either headphones or speakers. + 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 + /// 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) { + switch str { + case "BINAURAL": self = .binaural + case "IMMERSIVE": self = .immersive + case "DOWNMIX": self = .downmix + default: self = .unrecognized(String(str)) + } + } + } + + 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: specialUsageIdentifiers = Self.parseSpecialUsageIdentifiers(str: str) + 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] { + str.split(separator: ",").map { SpecialUsageIdentifier(str: $0) } + } +} + /// Represents a RFC6381 codec /// /// We are currently not parsing these values further @@ -282,4 +437,70 @@ 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: [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: VideoLayoutIdentifier + + public enum VideoLayoutIdentifier: RawRepresentable, Equatable { + /// Monoscopic. + /// + /// Indicates that a single image is present. + case chMono + /// Stereoscopic. + /// + /// Indicates that both left and right eye images are present. + 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) { + switch str { + case "CH-MONO": self = .chMono + case "CH-STEREO": self = .chStereo + default: self = .unrecognized(String(str)) + } + } + } + + public init?(failableInitWithString string: String) { + let layouts = string.split(separator: ",").map { VideoLayoutIdentifier(str: $0) } + guard let firstLayout = layouts.first else { + return nil + } + self.predominantLayout = firstLayout + self.layouts = layouts + } + + public init?(layouts: [VideoLayoutIdentifier]) { + guard let predominantLayout = layouts.first else { return nil } + self.layouts = layouts + self.predominantLayout = predominantLayout + } + + public func contains(_ layout: VideoLayoutIdentifier) -> Bool { + layouts.contains(layout) + } +} diff --git a/mambaTests/PantosTagTests.swift b/mambaTests/PantosTagTests.swift index 1d12b68..bd97656 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) @@ -75,6 +78,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 e2271e1..10587a2 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() { @@ -237,6 +354,7 @@ class GenericDictionaryTagValidatorTests: XCTestCase { .type, .groupId, .name, + .stableRenditionId, .language, .assocLanguage, .uri, @@ -244,15 +362,20 @@ 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, + .channels] + validate(tag: PantosTag.EXT_X_MEDIA, tagData: tagData, optional: optional, @@ -279,96 +402,368 @@ 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, + .hdcpLevel, + .videoRange] + validate(tag: PantosTag.EXT_X_STREAM_INF, tagData: tagData, optional: optional, @@ -465,57 +860,272 @@ 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] - + .resolution, + .hdcpLevel, + .videoRange] + validate(tag: PantosTag.EXT_X_I_FRAME_STREAM_INF, tagData: tagData, optional: optional, 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: [.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: [.format]) + + // 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: [.method]) + + 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/Util Tests/Value Types/ChannelsTests.swift b/mambaTests/Util Tests/Value Types/ChannelsTests.swift new file mode 100644 index 0000000..b2dd533 --- /dev/null +++ b/mambaTests/Util Tests/Value Types/ChannelsTests.swift @@ -0,0 +1,136 @@ +// +// 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) + } + + func test_sixChannelUnknownSpecialUsageIdentifier() { + let actualChannels = Channels(failableInitWithString: sixChannelUnknownSpecialUsageIdentifier) + 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 new file mode 100644 index 0000000..1a0ebb8 --- /dev/null +++ b/mambaTests/Util Tests/Value Types/VideoLayoutTests.swift @@ -0,0 +1,98 @@ +// +// 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 unrecognizedVideoLayout = "CH-TRI" + let monoLayout = "CH-MONO" + let stereoLayout = "CH-STEREO" + let stereoWithMonoLayout = "CH-STEREO,CH-MONO" + let monoWithStereoLayout = "CH-MONO,CH-STEREO" + let monoWithStereoWithUnrecognizedLayout = "CH-MONO,CH-STEREO,CH-TRI" + + func test_empty() { + XCTAssertNil(VideoLayout(failableInitWithString: empty)) + } + + 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() { + 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.contains(.chMono)) + XCTAssertFalse(videoLayout.contains(.chStereo)) + } + + 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.contains(.chMono)) + XCTAssertTrue(videoLayout.contains(.chStereo)) + } + + 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.contains(.chMono)) + XCTAssertTrue(videoLayout.contains(.chStereo)) + } + + 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.contains(.chMono)) + XCTAssertTrue(videoLayout.contains(.chStereo)) + } + + 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"))) + } +} diff --git a/mambaTests/ValidatorTests.swift b/mambaTests/ValidatorTests.swift index a0462af..23eab83 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,44 @@ 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) + ] + ) + } + + 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"