Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Candidate/1.6.0 #141

Merged
merged 34 commits into from
Oct 4, 2024
Merged
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
37d2468
Added failing test for media sequence calculation with skip
theRealRobG Aug 31, 2024
e8bf3ac
Added EXT-X-SKIP tag
theRealRobG Aug 31, 2024
ea3fa9c
Accounted for skip tags in media sequence calculation
theRealRobG Aug 31, 2024
9f03dc7
Adding multivariant playlist tags defined in draft 15 and still missi…
theRealRobG Sep 1, 2024
84b3a0b
Add attributes missing from EXT-X-MEDIA as of draft 15
theRealRobG Sep 2, 2024
63aaabc
Updated test comment with up-to-date docs for EXT-X-MEDIA
theRealRobG Sep 2, 2024
28d401a
Add attributes missing from EXT-X-STREAM-INF as of draft 15
theRealRobG Sep 2, 2024
064b48d
Add attributes missing from EXT-X-I-FRAME-STREAM-INF as of draft 15
theRealRobG Sep 2, 2024
9390aed
Updated encryption method enum with new type
theRealRobG Sep 2, 2024
a46b9e4
Added parsing for CHANNELS attribute
theRealRobG Sep 2, 2024
2441804
Added parsing for HDCP-LEVEL
theRealRobG Sep 2, 2024
492bc60
Added parsing for VIDEO-RANGE
theRealRobG Sep 3, 2024
4fd3f9d
Added parsing for REQ-VIDEO-LAYOUT
theRealRobG Sep 3, 2024
32d0ec8
Added parsing for FORMAT in EXT-X-SESSION-DATA
theRealRobG Sep 3, 2024
cdd2694
Corrected location for EXT-X-SESSION-DATA playlist validation
theRealRobG Sep 3, 2024
e6e31ca
Updated test sample manifest for readability
theRealRobG Sep 4, 2024
d7c4e21
Updated media sequence calculation logic to avoid unnecessary looping
theRealRobG Sep 4, 2024
0fdcbe6
Adding action to build and test iOS and tvOS targets
theRealRobG Sep 4, 2024
a234e9e
Attempting new way of defining iPhone device for testing
theRealRobG Sep 4, 2024
7d68c89
Added logging of env variables
theRealRobG Sep 4, 2024
4c90e1a
Cleaned up branches targeted for action
theRealRobG Sep 4, 2024
b79049c
Merge pull request #134 from Comcast/adding-github-actions
rmigneco Sep 4, 2024
f535098
Merge branch 'develop_1.x' into playlist-delta-updates
theRealRobG Sep 4, 2024
db05335
Merge pull request #130 from Comcast/playlist-delta-updates
rmigneco Sep 4, 2024
2f1eb23
Merge branch 'develop_1.x' into missing-multivariant-playlist-tags-fr…
theRealRobG Sep 4, 2024
c871de1
Favor structs over classes
theRealRobG Sep 7, 2024
f23275d
Favor not double declaring the issue variable
theRealRobG Sep 7, 2024
440593f
Clarified enum naming and favored more terse function syntax
theRealRobG Sep 7, 2024
34de4a9
Favor enums rather than structs holding enums
theRealRobG Sep 7, 2024
02d2cb9
Introduce unrecognized cases to keep parsing forward compatible
theRealRobG Sep 7, 2024
2d21125
Explicitly indicate class is final
theRealRobG Sep 9, 2024
9fbb538
Merge pull request #132 from Comcast/missing-multivariant-playlist-ta…
theRealRobG Sep 9, 2024
0c84c22
Merge branch 'develop_1.x' into candidate/1.6.0
theRealRobG Oct 4, 2024
d687e31
Update version to 1.6.0
theRealRobG Oct 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Build and test

on:
pull_request:
branches: [ "develop", "develop_1.x", "main", "main_1.x" ]

jobs:
define-ios-device:
name: Get iOS simulator device to run iOS tests on
runs-on: macos-latest
outputs:
device: ${{ steps.ios.outputs.device }}
steps:
- id: ios
run: echo "device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"`" >> "$GITHUB_OUTPUT"

build:
name: Build and Test mamba and mambaTVOS
runs-on: macos-latest
needs: define-ios-device
strategy:
matrix:
target:
- scheme: mamba
platform: iOS Simulator
device: ${{ needs.define-ios-device.outputs.device }}
- scheme: mambaTVOS
platform: tvOS Simulator
device: Apple TV
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build
env:
scheme: ${{ matrix.target.scheme }}
platform: ${{ matrix.target.platform }}
device: ${{ matrix.target.device }}
run: |
echo "scheme = $scheme"
echo "platform = $platform"
echo "device = $device"
xcodebuild build-for-testing -scheme "$scheme" -"workspace" "mamba.xcworkspace" -destination "platform=$platform,name=$device"
- name: Test
env:
scheme: ${{ matrix.target.scheme }}
platform: ${{ matrix.target.platform }}
device: ${{ matrix.target.device }}
run: |
echo "scheme = $scheme"
echo "platform = $platform"
echo "device = $device"
xcodebuild test-without-building -scheme "$scheme" -"workspace" "mamba.xcworkspace" -destination "platform=$platform,name=$device"
42 changes: 41 additions & 1 deletion mamba.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -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 /* HLSChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758382C8620C000D12CCD /* HLSChannelsTests.swift */; };
1447583A2C8620C000D12CCD /* HLSChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758382C8620C000D12CCD /* HLSChannelsTests.swift */; };
1447583B2C8620C000D12CCD /* HLSChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144758382C8620C000D12CCD /* HLSChannelsTests.swift */; };
1447583D2C8693E000D12CCD /* HLSVideoLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1447583C2C8693E000D12CCD /* HLSVideoLayoutTests.swift */; };
1447583E2C8693E000D12CCD /* HLSVideoLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1447583C2C8693E000D12CCD /* HLSVideoLayoutTests.swift */; };
1447583F2C8693E000D12CCD /* HLSVideoLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1447583C2C8693E000D12CCD /* HLSVideoLayoutTests.swift */; };
1D28F3451EAA9E500010320B /* hls_ad_master_playlist.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 1D28F3401EAA9E500010320B /* hls_ad_master_playlist.m3u8 */; };
1D28F3461EAA9E500010320B /* hls_ad_variant_playlist.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 1D28F3411EAA9E500010320B /* hls_ad_variant_playlist.m3u8 */; };
1D28F3471EAA9E500010320B /* hls_master_playlist_sap.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 1D28F3421EAA9E500010320B /* hls_master_playlist_sap.m3u8 */; };
@@ -617,6 +632,11 @@

/* Begin PBXFileReference section */
01CD2E791DE4D46F002510E7 /* EXT_X_MAPTagParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EXT_X_MAPTagParserTests.swift; sourceTree = "<group>"; };
1447582C2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXT_X_SESSION_KEYValidator.swift; sourceTree = "<group>"; };
144758302C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXT_X_SESSION_DATATagValidator.swift; sourceTree = "<group>"; };
144758342C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXT_X_SESSION_DATAPlaylistValidator.swift; sourceTree = "<group>"; };
144758382C8620C000D12CCD /* HLSChannelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HLSChannelsTests.swift; sourceTree = "<group>"; };
1447583C2C8693E000D12CCD /* HLSVideoLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HLSVideoLayoutTests.swift; sourceTree = "<group>"; };
1D28F3401EAA9E500010320B /* hls_ad_master_playlist.m3u8 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = hls_ad_master_playlist.m3u8; sourceTree = "<group>"; };
1D28F3411EAA9E500010320B /* hls_ad_variant_playlist.m3u8 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = hls_ad_variant_playlist.m3u8; sourceTree = "<group>"; };
1D28F3421EAA9E500010320B /* hls_master_playlist_sap.m3u8 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = hls_master_playlist_sap.m3u8; sourceTree = "<group>"; };
@@ -903,7 +923,6 @@
0173AB0D1D5BB371005DE51B /* Pantos-Generic Tag Validators */ = {
isa = PBXGroup;
children = (
EC95478A1E5CC86300962535 /* EXTINFValidator.swift */,
6DD0A1B0242FADC600FF7AAE /* EXT_X_DATERANGEPlaylistValidator.swift */,
6DD0A1AC242F85C800FF7AAE /* EXT_X_DATERANGETagValidator.swift */,
EC3B019E1DD4D47900B512E3 /* EXT_X_KEYValidator.swift */,
@@ -912,9 +931,13 @@
EC3B01A11DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupNAMEValidator.swift */,
EC3B01A21DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupTYPEValidator.swift */,
43DE4EFC1E564DBE00EEE800 /* EXT_X_MEDIARenditionINSTREAMIDValidator.swift */,
144758342C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift */,
144758302C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift */,
1447582C2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift */,
43DE4EFA1E564DA300EEE800 /* EXT_X_STARTTimeOffsetValidator.swift */,
EC3B01A31DD4D47900B512E3 /* EXT_X_STREAM_INFRenditionGroupValidator.swift */,
EC3B01A41DD4D47900B512E3 /* EXT_X_TARGETDURATIONLengthValidator.swift */,
EC95478A1E5CC86300962535 /* EXTINFValidator.swift */,
EC7491F51DD29DD300AF4E20 /* GenericDictionaryTagValidator.swift */,
EC7491F61DD29DD300AF4E20 /* GenericSingleTagValidator.swift */,
EC7491F71DD29DD300AF4E20 /* HLSDictionaryTagValueIdentifier.swift */,
@@ -1313,10 +1336,12 @@
EC9BCAA21D749D8B0032BEBE /* Value Types */ = {
isa = PBXGroup;
children = (
144758382C8620C000D12CCD /* HLSChannelsTests.swift */,
EC7492AF1DD29F8900AF4E20 /* HLSCodecArrayTests.swift */,
EC7492B01DD29F8900AF4E20 /* HLSMediaTypeTests.swift */,
EC7492B11DD29F8900AF4E20 /* HLSPlaylistTypeTests.swift */,
EC7492B21DD29F8900AF4E20 /* HLSResolutionTests.swift */,
1447583C2C8693E000D12CCD /* HLSVideoLayoutTests.swift */,
);
path = "Value Types";
sourceTree = "<group>";
@@ -1767,6 +1792,7 @@
ECFAA6581E6DD93C00398D66 /* HLSPlaylist.swift in Sources */,
ECC410601EA02F4800B4E3C8 /* StructureState.swift in Sources */,
EC7491811DD29C3500AF4E20 /* String+Trim.swift in Sources */,
144758352C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */,
EC7491C31DD29D5C00AF4E20 /* HLSValidationIssue.swift in Sources */,
EC74916E1DD29B5D00AF4E20 /* CollectionType+FindExtensions.swift in Sources */,
EC7491DA1DD29D9600AF4E20 /* GenericNoDataTagParser.swift in Sources */,
@@ -1836,7 +1862,9 @@
EC3B01AB1DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupNAMEValidator.swift in Sources */,
EC7491651DD29B0F00AF4E20 /* FailableStringLiteralConvertible.swift in Sources */,
EC7491461DD299B400AF4E20 /* HLSPlaylistTypes.swift in Sources */,
144758312C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift in Sources */,
EC3B01A71DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupAUTOSELECTValidator.swift in Sources */,
1447582D2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift in Sources */,
F700CD391E78A2BE001C9487 /* HLSStringRef_ConcreteNSString.m in Sources */,
43DE4EFB1E564DA300EEE800 /* EXT_X_STARTTimeOffsetValidator.swift in Sources */,
EC74918A1DD29CCB00AF4E20 /* StringDictionaryParser.swift in Sources */,
@@ -1914,11 +1942,13 @@
43DE4EFF1E564E1500EEE800 /* HLSMediaSpanTests.swift in Sources */,
ECE36DE41F2A9F10005E5DA7 /* HLSPlaylistTimelineAndSequencingTests.swift in Sources */,
01CD2E7A1DE4D46F002510E7 /* EXT_X_MAPTagParserTests.swift in Sources */,
144758392C8620C000D12CCD /* HLSChannelsTests.swift in Sources */,
EC7492481DD29E7300AF4E20 /* HLSValidatorTests.swift in Sources */,
EC7492AB1DD29F7000AF4E20 /* OrderedDictionaryTests.swift in Sources */,
EC74923E1DD29E7300AF4E20 /* HLSParser_Super8DemuxedTests.swift in Sources */,
EC7492781DD29EC800AF4E20 /* EXT_X_MEDIATagParserTests.swift in Sources */,
EC6F38921EA95882006BC30E /* HLSPlaylistInterfaceTests.swift in Sources */,
1447583D2C8693E000D12CCD /* HLSVideoLayoutTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1932,6 +1962,7 @@
ECFAA6591E6DD93C00398D66 /* HLSPlaylist.swift in Sources */,
ECC410611EA02F4800B4E3C8 /* StructureState.swift in Sources */,
EC3B01AA1DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupDEFAULTValidator.swift in Sources */,
144758362C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */,
EC3B01C41DD4D49A00B512E3 /* HLSPlaylistOneToManyValidator.swift in Sources */,
EC7491821DD29C3500AF4E20 /* String+Trim.swift in Sources */,
EC7491C41DD29D5C00AF4E20 /* HLSValidationIssue.swift in Sources */,
@@ -2001,7 +2032,9 @@
EC74917E1DD29C3500AF4E20 /* String+DateParsing.swift in Sources */,
EC3B01AC1DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupNAMEValidator.swift in Sources */,
EC7491661DD29B0F00AF4E20 /* FailableStringLiteralConvertible.swift in Sources */,
144758322C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift in Sources */,
EC7491471DD299B400AF4E20 /* HLSPlaylistTypes.swift in Sources */,
1447582E2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift in Sources */,
F700CD3A1E78A2BE001C9487 /* HLSStringRef_ConcreteNSString.m in Sources */,
EC3B01A81DD4D47900B512E3 /* EXT_X_MEDIARenditionGroupAUTOSELECTValidator.swift in Sources */,
EC74918B1DD29CCB00AF4E20 /* StringDictionaryParser.swift in Sources */,
@@ -2079,11 +2112,13 @@
43DE4F001E564E1500EEE800 /* HLSMediaSpanTests.swift in Sources */,
EC7492B41DD29F8900AF4E20 /* HLSCodecArrayTests.swift in Sources */,
ECE36DE51F2A9F10005E5DA7 /* HLSPlaylistTimelineAndSequencingTests.swift in Sources */,
1447583A2C8620C000D12CCD /* HLSChannelsTests.swift in Sources */,
EC7492491DD29E7300AF4E20 /* HLSValidatorTests.swift in Sources */,
EC7492AC1DD29F7000AF4E20 /* OrderedDictionaryTests.swift in Sources */,
EC74923F1DD29E7300AF4E20 /* HLSParser_Super8DemuxedTests.swift in Sources */,
EC8A3C801F7C329900A50EED /* HLSPlaylistStructureMasterTests.swift in Sources */,
EC7492791DD29EC800AF4E20 /* EXT_X_MEDIATagParserTests.swift in Sources */,
1447583E2C8693E000D12CCD /* HLSVideoLayoutTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -2097,6 +2132,7 @@
EC1CCD36209A2CF9006B59FF /* URL+hlsplaylist.swift in Sources */,
EC1CCD32209A2CF9006B59FF /* String+Trim.swift in Sources */,
EC1CCD46209A2CF9006B59FF /* GenericSingleTagValidator.swift in Sources */,
144758372C83D23100D12CCD /* EXT_X_SESSION_DATAPlaylistValidator.swift in Sources */,
EC1CCD59209A2CF9006B59FF /* HLSParser.swift in Sources */,
EC1CCD30209A2CF9006B59FF /* String+DateParsing.swift in Sources */,
EC1CCD53209A2CF9006B59FF /* GenericDictionaryTagWriter.swift in Sources */,
@@ -2166,7 +2202,9 @@
EC1CCD4F209A2CF9006B59FF /* HLSPlaylistTagCardinalityValidation.swift in Sources */,
EC1CCD2B209A2CF9006B59FF /* IndeterminateBool.swift in Sources */,
EC1CCCF9209A2CF9006B59FF /* HLSTagCriterion.swift in Sources */,
144758332C83C72B00D12CCD /* EXT_X_SESSION_DATATagValidator.swift in Sources */,
EC1CCD43209A2CF9006B59FF /* EXT_X_STREAM_INFRenditionGroupValidator.swift in Sources */,
1447582F2C83C20800D12CCD /* EXT_X_SESSION_KEYValidator.swift in Sources */,
EC1CCD35209A2CF9006B59FF /* StringDictionaryParser.swift in Sources */,
EC1CCD02209A2CF9006B59FF /* HLSStringRef_ConcreteNSString.m in Sources */,
EC1CCD38209A2CF9006B59FF /* GenericNoDataTagParser.swift in Sources */,
@@ -2244,11 +2282,13 @@
ECE253FD209A50B500D388CE /* ThirdPartyTagListSupportTests.swift in Sources */,
ECE25408209A50B500D388CE /* HLSResolutionTests.swift in Sources */,
ECE253F6209A50B500D388CE /* GenericSingleValueTagParserTests.swift in Sources */,
1447583B2C8620C000D12CCD /* HLSChannelsTests.swift in Sources */,
ECE253FA209A50B500D388CE /* GenericSingleTagValidatorTests.swift in Sources */,
ECE253DF209A509900D388CE /* HLSParserTest.swift in Sources */,
ECE253F1209A50B500D388CE /* EXT_X_KEYTagParserTests.swift in Sources */,
ECE25403209A50B500D388CE /* String+Helio.swift in Sources */,
ECE25400209A50B500D388CE /* IndeterminateBoolTests.swift in Sources */,
1447583F2C8693E000D12CCD /* HLSVideoLayoutTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Original file line number Diff line number Diff line change
@@ -372,16 +372,44 @@ fileprivate struct HLSPlaylistStructureConstructor {
var currentSegmentDuration: CMTime = CMTime.invalid
var discontinuity = false
let tagDescriptor = self.tagDescriptor(forTags: tags)

// figure out our media sequence start (defaults to 1 if not specified)
let mediaSequenceTags = tags.filter{ $0.tagDescriptor == PantosTag.EXT_X_MEDIA_SEQUENCE }
if mediaSequenceTags.count > 0 {
assert(mediaSequenceTags.count == 1, "Unexpected to have more than one media sequence")
if let startMediaSequence: MediaSequence = mediaSequenceTags.first?.value(forValueIdentifier: PantosValue.sequence) {
currentMediaSequence = startMediaSequence

// collect media sequence and skip tag (if they exist) as they impact the initial media sequence value
var mediaSequenceTag: HLSTag?
var skipTag: HLSTag?
for tag in tags {
switch tag.tagDescriptor {
case PantosTag.EXT_X_MEDIA_SEQUENCE: mediaSequenceTag = tag
case PantosTag.EXT_X_SKIP: skipTag = tag
case PantosTag.Location:
// Both the EXT-X-MEDIA-SEQUNCE and the EXT-X-SKIP tag are expected to occur before any Media Segments.
//
// For EXT-X-MEDIA-SEQUNCE section 4.4.3.2 indicates:
// The EXT-X-MEDIA-SEQUENCE tag MUST appear before the first Media Segment in the Playlist.
//
// For EXT-X-SKIP section 4.4.5.2 indicates:
// A server produces a Playlist Delta Update (Section 6.2.5.1), by replacing tags earlier than the
// Skip Boundary with an EXT-X-SKIP tag. When replacing Media Segments, the EXT-X-SKIP tag replaces
// the segment URI lines and all Media Segment Tags tags that are applied to those segments.
//
// Exiting early at the first Location helps us avoid having to loop through the entire playlist when we
// know that the tags we're looking for MUST NOT exist.
break
default: continue
}
}


// figure out our media sequence start (defaults to 0 if not specified)
if let startMediaSequence: MediaSequence = mediaSequenceTag?.value(forValueIdentifier: PantosValue.sequence) {
currentMediaSequence = startMediaSequence
}

// account for any skip tag (since a delta update replaces all segments earlier than the skip boundary, the
// SKIPPED-SEGMENTS value will effectively update the current media sequence value of the first segment, so safe
// to do this here and not within the looping through media group tags below).
if let skippedSegments: Int = skipTag?.value(forValueIdentifier: PantosValue.skippedSegments) {
currentMediaSequence += skippedSegments
}

// find the "header" portion by finding the first ".mediaSegment" scoped tag
let mediaStartIndexOptional = tags.firstIndex(where: { $0.scope() == .mediaSegment })

3 changes: 3 additions & 0 deletions mambaSharedFramework/HLSValidationIssue.swift
Original file line number Diff line number Diff line change
@@ -48,6 +48,9 @@ public enum IssueDescription: String {

case HLSPlaylistRenditionGroupMatchingNAMELANGUAGEValidator = "A Playlist MAY contain multiple groups of the same TYPE in order to provide multiple encodings of each rendition. If it does so, each group of the same TYPE SHOULD contain corresponding members with the same NAME attribute, LANGUAGE attribute, and rendition."
case EXT_X_KEYValidator = "EXT-X-KEY If the encryption method is NONE, the URI, IV, KEYFORMAT and KEYFORMATVERSIONS attributes MUST NOT be present. If the encryption method is AES-128 or SAMPLE-AES, the URI attribute MUST be present."
case EXT_X_SESSION_KEYValidator = "All attributes defined for the EXT-X-KEY tag are also defined for the EXT-X-SESSION-KEY, except that the value of the METHOD attribute MUST NOT be NONE."
case EXT_X_SESSION_DATATagValidator = "Each EXT-X-SESSION-DATA tag MUST contain either a VALUE or URI attribute, but not both."
case EXT_X_SESSION_DATAPlaylistValidator = "A Playlist MAY contain multiple EXT-X-SESSION-DATA tags with the same DATA-ID attribute. A Playlist MUST NOT contain more than one EXT-X-SESSION-DATA tag with the same DATA-ID attribute and the same LANGUAGE attribute."
case HLSPlaylistRenditionGroupMatchingPROGRAM_IDValidator = "Variant Playlists MUST contain an EXT-X-STREAM-INF tag or EXT-X-I-FRAME-STREAM-INF tag for each variant stream. Each tag identifying an encoding of the same presentation MUST have the same PROGRAM-ID attribute value."
case EXT_X_STREAM_INFRenditionGroupAUDIOValidator = "EXT-X-STREAM-INF - AUDIO The value is a quoted-string. It MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Playlist whose TYPE attribute is AUDIO."
case EXT_X_STREAM_INFRenditionGroupVIDEOValidator = "EXT-X-STREAM-INF - VIDEO The value is a quoted-string. It MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Playlist whose TYPE attribute is VIDEO."
227 changes: 224 additions & 3 deletions mambaSharedFramework/HLSValueTypes.swift
Original file line number Diff line number Diff line change
@@ -104,13 +104,14 @@ public func ==(lhs: HLSMediaType, rhs: HLSMediaType) -> Bool {

/// Represents an encryption method
///
/// Can be initialized with a string "NONE" or "AES-128" or "SAMPLE-AES" for a valid value
/// Can be initialized with a string "NONE" or "AES-128" or "SAMPLE-AES" or "SAMPLE-AES-CTR" for a valid value
public struct HLSEncryptionMethodType: Equatable, FailableStringLiteralConvertible {
public let type: EncryptionMethod
public enum EncryptionMethod: String {
case None = "NONE"
case AES128 = "AES-128"
case SampleAES = "SAMPLE-AES"
case SampleAESCTR = "SAMPLE-AES-CTR"
}
public init?(string: String) {
self.init(encryption: string)
@@ -130,6 +131,59 @@ public func ==(lhs: HLSEncryptionMethodType, rhs: HLSEncryptionMethodType) -> Bo
return lhs.type == rhs.type
}

/// Represents a minimum required HDCP level needed to play content.
public enum HLSHDCPLevel: String, Equatable, FailableStringLiteralConvertible {
/// Indicates that the content does not require output copy protections.
case none = "NONE"
/// Indicates that the Variant Stream could fail to play unless the output is protected by High-bandwidth Digital
/// Content Protection (HDCP) Type 0 or equivalent.
case type0 = "TYPE-0"
/// Indicates that the Variant Stream could fail to play unless the output is protected by HDCP Type 1 or
/// equivalent.
case type1 = "TYPE-1"

public init?(string: String) {
self.init(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 HLSVideoRange: String, Equatable, FailableStringLiteralConvertible {
/// The value MUST be SDR if the video in the Variant Stream is encoded using one of the following reference
/// opto-electronic transfer characteristic functions specified by the TransferCharacteristics code point: 1, 6, 13,
/// 14, 15. Note that different TransferCharacteristics code points can use the same transfer function.
case sdr = "SDR"
/// The value MUST be HLG if the video in the Variant Stream is encoded using a reference opto-electronic transfer
/// characteristic function specified by the TransferCharacteristics code point 18, or consists of such video mixed
/// with video qualifying as SDR.
case hlg = "HLG"
/// The value MUST be PQ if the video in the Variant Stream is encoded using a reference opto-electronic transfer
/// characteristic function specified by the TransferCharacteristics code point 16, or consists of such video mixed
/// with video qualifying as SDR or HLG.
case pq = "PQ"

public init?(string: String) {
self.init(rawValue: string)
}
}

/// Represents the format of the file referenced by `EXT-X-SESSION-DATA:URI`.
public enum HLSSessionDataFormat: String, Equatable, FailableStringLiteralConvertible {
case json = "JSON"
case raw = "RAW"

public init?(string: String) {
self.init(rawValue: string)
}
}

/// Represents a playlist type
///
/// Can be initialized with a string "EVENT" or "VOD" for a valid value
@@ -160,7 +214,6 @@ public func ==(lhs: HLSPlaylistType, rhs: HLSPlaylistType) -> Bool {
/// Represents a instreamId type
///
/// Can be initialized with a string "CC1" or "CC2" or "CC3" or "CC4" for a valid value

public enum HLSInstreamId: String, FailableStringLiteralConvertible {
case CC1 = "CC1"
case CC2 = "CC2"
@@ -173,7 +226,6 @@ public enum HLSInstreamId: String, FailableStringLiteralConvertible {

}


/// Represents a CLOSED-CAPTIONS
///
/// can be either a quoted-string or an enumerated-string with the value NONE for a valid value
@@ -189,6 +241,109 @@ public struct HLSClosedCaptions: FailableStringLiteralConvertible {
}
}

/// Represents CHANNELS
public struct HLSChannels: Equatable, FailableStringLiteralConvertible {
/// A count of audio channels, indicating the maximum number of independent, simultaneous audio channels present in
/// any Media Segment in the Rendition.
///
/// For example, an AC-3 5.1 Rendition would have a CHANNELS="6" attribute.
public let count: Int
/// Identifies the presence of spatial audio of some kind, for example, object-based audio, in the Rendition. The
/// Audio Coding Identifiers are codec-specific.
public let spatialAudioCodingIdentifiers: [String]
/// Supplementary indications of special channel usage that are necessary for informed selection and processing.
/// This parameter is an array of Special Usage Identifiers.
public let specialUsageIdentifiers: [SpecialUsageIdentifier]

public enum SpecialUsageIdentifier: 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?(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(string: String(str))
}

private static func parseSpatialAudioCodingIdentifiers(str: Substring) -> [String] {
let split = str.split(separator: ",")
var identifiers = [String]()
for id in split where id != "-" {
identifiers.append(String(id))
}
return identifiers
}

private static func parseSpecialUsageIdentifiers(str: Substring) -> [SpecialUsageIdentifier] {
str.split(separator: ",").map { SpecialUsageIdentifier(str: $0) }
}
}

/// Represents a RFC6381 codec
///
/// We are currently not parsing these values further
@@ -266,4 +421,70 @@ public func ==(lhs: HLSCodecArray, rhs: HLSCodecArray) -> Bool {
return lhs.codecs == rhs.codecs
}

/// Represents information to assist in view presentation.
///
/// Indicates when video content in the Variant Stream requires specialized rendering to be properly displayed.
public struct HLSVideoLayout: Equatable, FailableStringLiteralConvertible {
/// Each specifier controls one aspect of the entry. That is, the specifiers are disjoint and the values for a
/// specifier are mutually exclusive.
public let layouts: [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?(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)
}
}
Original file line number Diff line number Diff line change
@@ -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: HLSPlaylistValidator {
static func validate(hlsPlaylist: any HLSPlaylistInterface) -> [HLSValidationIssue]? {
var issues = [HLSValidationIssue]()

if let issue = duplicateIssue(
tags: hlsPlaylist.tags.filter { $0.tagDescriptor == PantosTag.EXT_X_SESSION_DATA }
) {
issues.append(issue)
}

return issues.isEmpty ? nil : issues
}

// A Playlist MAY contain multiple EXT-X-SESSION-DATA tags with the same DATA-ID attribute. A Playlist MUST NOT
// contain more than one EXT-X-SESSION-DATA tag with the same DATA-ID attribute and the same LANGUAGE attribute.
private static func duplicateIssue(tags: [HLSTag]) -> HLSValidationIssue? {
var dataIdToLanguagesMap = [String: [String?]]()
for tag in tags {
guard let dataId = tag.value(forValueIdentifier: PantosValue.dataId) else { continue }
var existingLanguages = dataIdToLanguagesMap[dataId] ?? []
existingLanguages.append(tag.value(forValueIdentifier: PantosValue.language))
dataIdToLanguagesMap[dataId] = existingLanguages
}
for languages in dataIdToLanguagesMap.values {
if languages.count != Set(languages).count {
return HLSValidationIssue(description: .EXT_X_SESSION_DATAPlaylistValidator, severity: .error)
}
}
return nil
}
}
Original file line number Diff line number Diff line change
@@ -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: HLSTagValidator {
private var genericValidator: GenericDictionaryTagValidator

init() {
genericValidator = GenericDictionaryTagValidator(
tag: PantosTag.EXT_X_SESSION_DATA,
dictionaryValueIdentifiers: [
HLSDictionaryTagValueIdentifierImpl(
valueId: PantosValue.dataId,
optional: false,
expectedType: String.self
),
HLSDictionaryTagValueIdentifierImpl(
valueId: PantosValue.value,
optional: true,
expectedType: String.self
),
HLSDictionaryTagValueIdentifierImpl(
valueId: PantosValue.uri,
optional: true,
expectedType: String.self
),
HLSDictionaryTagValueIdentifierImpl(
valueId: PantosValue.format,
optional: true,
expectedType: HLSSessionDataFormat.self
),
HLSDictionaryTagValueIdentifierImpl(
valueId: PantosValue.language,
optional: true,
expectedType: String.self
),
]
)
}

func validate(tag: HLSTag) -> [HLSValidationIssue]? {
var issueList = genericValidator.validate(tag: tag) ?? []

// Each EXT-X-SESSION-DATA tag MUST contain either a VALUE or URI attribute, but not both.
switch (tag.value(forValueIdentifier: PantosValue.value), tag.value(forValueIdentifier: PantosValue.uri)) {
case (.none, .some), (.some, .none):
break
case (.some, .some), (.none, .none):
issueList.append(HLSValidationIssue(description: .EXT_X_SESSION_DATATagValidator, severity: .error))
}

return issueList.isEmpty ? nil : issueList
}
}
Original file line number Diff line number Diff line change
@@ -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: HLSTagValidator {
private let keyValidator: EXT_X_KEYValidator

init() {
keyValidator = EXT_X_KEYValidator(tag: PantosTag.EXT_X_SESSION_KEY, dictionaryValueIdentifiers: [
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.method,
optional: false,
expectedType: HLSEncryptionMethodType.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.uri,
optional: false, // URI is REQUIRED since METHOD can't be NONE
expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.ivector,
optional: true,
expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.keyformat,
optional: true,
expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.keyformatVersions,
optional: true,
expectedType: String.self)
])
}

public func validate(tag: HLSTag) -> [HLSValidationIssue]? {
var issueList = keyValidator.validate(tag: tag) ?? []

if let method = tag.value(forValueIdentifier: PantosValue.method) {
if method == HLSEncryptionMethodType.EncryptionMethod.None.rawValue {
issueList.append(
HLSValidationIssue(
description: IssueDescription.EXT_X_SESSION_KEYValidator,
severity: IssueSeverity.error
)
)
}
}

return issueList.isEmpty ? nil : issueList
}
}
Original file line number Diff line number Diff line change
@@ -76,7 +76,8 @@ public class HLSMasterPlaylistValidator: HLSExtensibleValidator {
public static let validators:[HLSValidator.Type] = [HLSPlaylistRenditionGroupValidator.self,
EXT_X_STREAM_INFRenditionGroupAUDIOValidator.self,
EXT_X_STREAM_INFRenditionGroupVIDEOValidator.self,
EXT_X_STREAM_INFRenditionGroupSUBTITLESValidator.self]
EXT_X_STREAM_INFRenditionGroupSUBTITLESValidator.self,
EXT_X_SESSION_DATAPlaylistValidator.self]
}

/// Validator for variant playlists
Original file line number Diff line number Diff line change
@@ -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"
@@ -77,6 +80,7 @@ public enum PantosTag: String {

// MARK: Variant playlist - Media metadata tags
case EXT_X_DATERANGE = "EXT-X-DATERANGE"
case EXT_X_SKIP = "EXT-X-SKIP"
}

extension PantosTag: HLSTagDescriptor, Equatable {
@@ -128,6 +132,12 @@ extension PantosTag: HLSTagDescriptor, Equatable {
fallthrough
case .EXT_X_I_FRAME_STREAM_INF:
fallthrough
case .EXT_X_SESSION_DATA:
fallthrough
case .EXT_X_SESSION_KEY:
fallthrough
case .EXT_X_CONTENT_STEERING:
fallthrough
case .EXT_X_ENDLIST:
fallthrough
case .EXT_X_INDEPENDENT_SEGMENTS:
@@ -139,6 +149,8 @@ extension PantosTag: HLSTagDescriptor, Equatable {
case .EXT_X_TARGETDURATION:
fallthrough
case .EXT_X_DATERANGE:
fallthrough
case .EXT_X_SKIP:
return .wholePlaylist

case .EXT_X_BITRATE:
@@ -193,6 +205,12 @@ extension PantosTag: HLSTagDescriptor, Equatable {

case .EXT_X_I_FRAME_STREAM_INF:
fallthrough
case .EXT_X_SESSION_DATA:
fallthrough
case .EXT_X_SESSION_KEY:
fallthrough
case .EXT_X_CONTENT_STEERING:
fallthrough
case .EXT_X_MEDIA:
fallthrough
case .EXT_X_STREAM_INF:
@@ -204,6 +222,8 @@ extension PantosTag: HLSTagDescriptor, Equatable {
case .EXT_X_KEY:
fallthrough
case .EXT_X_DATERANGE:
fallthrough
case .EXT_X_SKIP:
return .keyValue

case .Location:
@@ -271,13 +291,21 @@ extension PantosTag: HLSTagDescriptor, Equatable {
fallthrough
case .EXT_X_I_FRAME_STREAM_INF:
fallthrough
case .EXT_X_SESSION_DATA:
fallthrough
case .EXT_X_SESSION_KEY:
fallthrough
case .EXT_X_CONTENT_STEERING:
fallthrough
case .EXT_X_MAP:
fallthrough
case .EXT_X_START:
fallthrough
case .EXT_X_KEY:
fallthrough
case .EXT_X_DATERANGE:
fallthrough
case .EXT_X_SKIP:
return GenericDictionaryTagParser(tag: pantostag)

// No Data tags
@@ -335,13 +363,21 @@ extension PantosTag: HLSTagDescriptor, Equatable {
fallthrough
case .EXT_X_I_FRAME_STREAM_INF:
fallthrough
case .EXT_X_SESSION_DATA:
fallthrough
case .EXT_X_SESSION_KEY:
fallthrough
case .EXT_X_CONTENT_STEERING:
fallthrough
case .EXT_X_MAP:
fallthrough
case .EXT_X_START:
fallthrough
case .EXT_X_KEY:
fallthrough
case .EXT_X_DATERANGE:
fallthrough
case .EXT_X_SKIP:
return GenericDictionaryTagWriter()

// These tags cannot be modified and therefore these cases are invalid.
@@ -399,13 +435,23 @@ extension PantosTag: HLSTagDescriptor, Equatable {
case .EXT_X_STREAM_INF:
return GenericDictionaryTagValidator(tag: pantostag, dictionaryValueIdentifiers: [
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.bandwidthBPS, optional: false, expectedType: Int.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.averageBandwidthBPS, optional: true, expectedType: Int.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.score, optional: true, expectedType: Double.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.programId, optional: true, expectedType: Int.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.codecs, optional: true, expectedType: HLSCodecArray.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.supplementalCodecs, optional: true, expectedType: HLSCodecArray.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.resolution, optional: true, expectedType: HLSResolution.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.frameRate, optional: true, expectedType: Double.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: HLSHDCPLevel.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: HLSVideoRange.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: HLSVideoLayout.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.stableVariantId, optional: true, expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.audioGroup, optional: true, expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoGroup, optional: true, expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.subtitlesGroup, optional: true, expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.closedCaptionsGroup, optional: true, expectedType: HLSClosedCaptions.self)
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.closedCaptionsGroup, optional: true, expectedType: HLSClosedCaptions.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.pathwayId, optional: true, expectedType: String.self)
])

case .EXT_X_MEDIA:
@@ -416,23 +462,52 @@ extension PantosTag: HLSTagDescriptor, Equatable {
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.language, optional: true, expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.assocLanguage, optional: true, expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.name, optional: true, expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.stableRenditionId, optional: true, expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.defaultMedia, optional: true, expectedType: Bool.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.autoselect, optional: true, expectedType: Bool.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.forced, optional: true, expectedType: Bool.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.instreamId, optional: true, expectedType: HLSInstreamId.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.characteristics, optional: true, expectedType: String.self)
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.bitDepth, optional: true, expectedType: Int.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.sampleRate, optional: true, expectedType: Int.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.characteristics, optional: true, expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.channels, optional: true, expectedType: HLSChannels.self)
])

case .EXT_X_I_FRAME_STREAM_INF:
return GenericDictionaryTagValidator(tag: pantostag, dictionaryValueIdentifiers: [
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.uri, optional: false, expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.bandwidthBPS, optional: false, expectedType: Int.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.averageBandwidthBPS, optional: true, expectedType: Int.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.score, optional: true, expectedType: Double.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.programId, optional: true, expectedType: Int.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.codecs, optional: true, expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.codecs, optional: true, expectedType: HLSCodecArray.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.supplementalCodecs, optional: true, expectedType: HLSCodecArray.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.resolution, optional: true, expectedType: HLSResolution.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.uri, optional: false, expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.hdcpLevel, optional: true, expectedType: HLSHDCPLevel.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.allowedCpc, optional: true, expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoRange, optional: true, expectedType: HLSVideoRange.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.reqVideoLayout, optional: true, expectedType: HLSVideoLayout.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.stableVariantId, optional: true, expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.videoGroup, optional: true, expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.pathwayId, optional: true, expectedType: String.self),
])


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: [
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.serverUri,
optional: false,
expectedType: String.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.pathwayId,
optional: true,
expectedType: String.self)
])

case .EXT_X_KEY:
return EXT_X_KEYValidator(tag: pantostag, dictionaryValueIdentifiers: [
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.method, optional: true, expectedType: HLSEncryptionMethodType.self),
@@ -464,7 +539,17 @@ extension PantosTag: HLSTagDescriptor, Equatable {

case .EXT_X_DATERANGE:
return EXT_X_DATERANGETagValidator()


case .EXT_X_SKIP:
return GenericDictionaryTagValidator(tag: pantostag, dictionaryValueIdentifiers: [
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.skippedSegments,
optional: false,
expectedType: Int.self),
HLSDictionaryTagValueIdentifierImpl(valueId: PantosValue.recentlyRemovedDateranges,
optional: true,
expectedType: String.self)
])

case .Location:
return nil

@@ -493,6 +578,9 @@ extension PantosTag: HLSTagDescriptor, Equatable {
PantosTag.EXT_X_VERSION,
PantosTag.EXT_X_MEDIA,
PantosTag.EXT_X_I_FRAME_STREAM_INF,
PantosTag.EXT_X_SESSION_DATA,
PantosTag.EXT_X_SESSION_KEY,
PantosTag.EXT_X_CONTENT_STEERING,
PantosTag.EXT_X_TARGETDURATION,
PantosTag.EXT_X_MEDIA_SEQUENCE,
PantosTag.EXT_X_ENDLIST,
@@ -510,7 +598,8 @@ extension PantosTag: HLSTagDescriptor, Equatable {
PantosTag.EXT_X_START,
PantosTag.EXT_X_DISCONTINUITY,
PantosTag.EXT_X_BITRATE,
PantosTag.EXT_X_DATERANGE]
PantosTag.EXT_X_DATERANGE,
PantosTag.EXT_X_SKIP]

var dictionary = [UInt: [(descriptor: PantosTag, string: HLSStringRef)]]()

Original file line number Diff line number Diff line change
@@ -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
@@ -189,7 +248,22 @@ public enum PantosValue: String {
/// after the START-DATE of the range in question. This attribute is
/// OPTIONAL.
case endOnNext = "END-ON-NEXT"


/// Found in `.EXT_X_SKIP`.
///
/// The value is a decimal-integer specifying the number of Media
/// Segments replaced by the EXT-X-SKIP tag. This attribute is
/// REQUIRED.
case skippedSegments = "SKIPPED-SEGMENTS"

/// Found in `.EXT_X_SKIP`.
///
/// The value is a quoted-string consisting of a tab (0x9) delimited
/// list of EXT-X-DATERANGE IDs that have been removed from the
/// Playlist recently. See Section 6.2.5.1 for more information.
/// This attribute is REQUIRED if the Client requested an update that
/// skips EXT-X-DATERANGE tags. The quoted-string MAY be empty.
case recentlyRemovedDateranges = "RECENTLY-REMOVED-DATERANGES"
}

extension PantosValue: HLSTagValueIdentifier {
42 changes: 42 additions & 0 deletions mambaTests/HLSPlaylistStructureAndEditingTests.swift
Original file line number Diff line number Diff line change
@@ -832,6 +832,26 @@ class HLSPlaylistStructureAndEditingTests: XCTestCase {
XCTAssert(playlist.footer?.range.count == 1, "Should have a footer")
XCTAssert(playlist.mediaSpans.count == 2, "Should have 2 spans")
}

func testDeltaUpdateCorrectlyCalculatesMediaSequencesInTagGroups() {
let playlist = parsePlaylist(inString: sampleDeltaUpdatePlaylist)

XCTAssertEqual(playlist.header?.range.count, 5, "Should have a header including 'server-control' and 'skip'")
XCTAssertEqual(playlist.mediaSegmentGroups.count, 6, "Should have 6 remaining groups")
for i in 0..<6 {
guard playlist.mediaSegmentGroups.indices.contains(i) else {
return XCTFail("Should have media segment group at index \(i)")
}
let group = playlist.mediaSegmentGroups[i]
XCTAssertEqual(
group.mediaSequence,
i + 5,
"Should have media sequence value equal to index (\(i)) + initial media sequence (1) + skipped (4)"
)
}
XCTAssertNil(playlist.footer, "Should have no footer")
XCTAssertEqual(playlist.mediaSpans.count, 0, "Should have no spans (no key tags)")
}
}


@@ -937,3 +957,25 @@ let sample4SegmentPlaylist =
"#EXTINF:2.002,\n" +
"http://not.a.server.nowhere/segment4.ts\n" +
"#EXT-X-ENDLIST\n"

let sampleDeltaUpdatePlaylist =
"""
#EXTM3U
#EXT-X-VERSION:9
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=12
#EXT-X-TARGETDURATION:2
#EXT-X-SKIP:SKIPPED-SEGMENTS=4
#EXTINF:2.002,
http://not.a.server.nowhere/segment5.ts
#EXTINF:2.002,
http://not.a.server.nowhere/segment6.ts
#EXTINF:2.002,
http://not.a.server.nowhere/segment7.ts
#EXTINF:2.002,
http://not.a.server.nowhere/segment8.ts
#EXTINF:2.002,
http://not.a.server.nowhere/segment9.ts
#EXTINF:2.002,
http://not.a.server.nowhere/segment10.ts
"""
37 changes: 36 additions & 1 deletion mambaTests/HLSValidatorTests.swift
Original file line number Diff line number Diff line change
@@ -785,5 +785,40 @@ frag1.ts
let expectedIssues = [HLSValidationIssue(description: .EXT_X_DATERANGEAttributeMismatchForTagsWithSameID, severity: .warning)]
validate(validator: u, playlist: hlsLoadString, expectedIssues: expectedIssues)
}


func testEXT_X_SESSION_DATAPlaylistValidator_multipleSessionDataDifferentLanguageIsOK() {
var tags = EXT_X_MEDIA_txt
tags.insert("#EXT-X-SESSION-DATA:DATA-ID=\"com.example.text\",VALUE=\"example\",LANGUAGE=\"en\"\n", at: 1)
tags.insert("#EXT-X-SESSION-DATA:DATA-ID=\"com.example.text\",VALUE=\"example\",LANGUAGE=\"es\"\n", at: 1)
let playlist = tags.joined()
validate(
validator: EXT_X_SESSION_DATAPlaylistValidator.self,
playlist: playlist,
expectedIssues: []
)
}

func testEXT_X_SESSION_DATAPlaylistValidator_multipleSessionDataSameLanguageIsNotOK() {
var tags = EXT_X_MEDIA_txt
tags.insert("#EXT-X-SESSION-DATA:DATA-ID=\"com.example.text\",VALUE=\"example\",LANGUAGE=\"en\"\n", at: 1)
tags.insert("#EXT-X-SESSION-DATA:DATA-ID=\"com.example.text\",VALUE=\"example\",LANGUAGE=\"en\"\n", at: 1)
let playlist = tags.joined()
validate(
validator: EXT_X_SESSION_DATAPlaylistValidator.self,
playlist: playlist,
expectedIssues: [HLSValidationIssue(description: .EXT_X_SESSION_DATAPlaylistValidator, severity: .error)]
)
}

func testEXT_X_SESSION_DATAPlaylistValidator_existsWithinMasterPlaylistValidators() {
XCTAssertEqual(
1,
HLSMasterPlaylistValidator.validators.filter { $0 == EXT_X_SESSION_DATAPlaylistValidator.self }.count
)
XCTAssertEqual(
0,
HLSVariantPlaylistValidator.validators.filter { $0 == EXT_X_SESSION_DATAPlaylistValidator.self }.count
)
}

}
12 changes: 12 additions & 0 deletions mambaTests/PantosTagTests.swift
Original file line number Diff line number Diff line change
@@ -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)

@@ -43,6 +46,7 @@ class PantosTagTests: XCTestCase {
runStringRefLookupTest(onPantosDescriptor: PantosTag.EXT_X_ENDLIST)
runStringRefLookupTest(onPantosDescriptor: PantosTag.EXT_X_BITRATE)
runStringRefLookupTest(onPantosDescriptor: PantosTag.EXT_X_DATERANGE)
runStringRefLookupTest(onPantosDescriptor: PantosTag.EXT_X_SKIP)

runStringRefLookupTest(onPantosDescriptor: PantosTag.EXT_X_INDEPENDENT_SEGMENTS)
runStringRefLookupTest(onPantosDescriptor: PantosTag.EXT_X_START)
@@ -74,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:
@@ -101,6 +111,8 @@ class PantosTagTests: XCTestCase {
case .EXTINF:
fallthrough
case .EXT_X_DATERANGE:
fallthrough
case .EXT_X_SKIP:
let stringRef = HLSStringRef(string: "#\(descriptor.toString())")
guard let newDescriptor = PantosTag.constructDescriptor(fromStringRef: stringRef) else {
XCTFail("PantosTag \(descriptor.toString()) is missing from stringRefLookup table.")
988 changes: 820 additions & 168 deletions mambaTests/Tag Validator Tests/GenericDictionaryTagValidatorTests.swift

Large diffs are not rendered by default.

136 changes: 136 additions & 0 deletions mambaTests/Util Tests/Value Types/HLSChannelsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//
// HLSChannelsTests.swift
// mamba
//
// Created by Robert Galluccio on 9/2/24.
// Copyright © 2024 Comcast Corporation.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License. All rights reserved.
//

import Foundation
import XCTest
@testable import mamba

class HLSChannelsTests: XCTestCase {
let empty = ""
let invalidCount = "ONE"
let sixChannel = "6"
let twelveChannelJoc = "12/JOC"
let twelveChannelJocAndUnknownSpatialCoding = "12/JOC,SPECIAL"
let sixChannelWithEmptySpatialIdentifier = "6/-"
let twelveChannelUnknownSpatialWithDashInName = "12/VERY-SPATIAL"
let sixChannelNoSpatialWithDownmix = "6/-/DOWNMIX"
let sixChannelNoSpatialWithBinauralAndImmersive = "6/-/BINAURAL,IMMERSIVE"
let twelveChannelJocAndImmersive = "12/JOC/IMMERSIVE"
let sixChannelUnknownSpecialUsageIdentifier = "6/-/NEW-IDENTIFIER"

func test_empty() {
let actualChannels = HLSChannels(string: empty)
XCTAssertNil(actualChannels)
}

func test_invalidCount() {
let actualChannels = HLSChannels(string: invalidCount)
XCTAssertNil(actualChannels)
}

func test_sixChannel() {
let actualChannels = HLSChannels(string: sixChannel)
let expectedChannels = HLSChannels(
count: 6,
spatialAudioCodingIdentifiers: [],
specialUsageIdentifiers: []
)
XCTAssertEqual(expectedChannels, actualChannels)
}

func test_twelveChannelJoc() {
let actualChannels = HLSChannels(string: twelveChannelJoc)
let expectedChannels = HLSChannels(
count: 12,
spatialAudioCodingIdentifiers: ["JOC"],
specialUsageIdentifiers: []
)
XCTAssertEqual(expectedChannels, actualChannels)
}

func test_twelveChannelJocAndUnknownSpatialCoding() {
let actualChannels = HLSChannels(string: twelveChannelJocAndUnknownSpatialCoding)
let expectedChannels = HLSChannels(
count: 12,
spatialAudioCodingIdentifiers: ["JOC", "SPECIAL"],
specialUsageIdentifiers: []
)
XCTAssertEqual(expectedChannels, actualChannels)
}

func test_sixChannelWithEmptySpatialIdentifier() {
let actualChannels = HLSChannels(string: sixChannelWithEmptySpatialIdentifier)
let expectedChannels = HLSChannels(
count: 6,
spatialAudioCodingIdentifiers: [],
specialUsageIdentifiers: []
)
XCTAssertEqual(expectedChannels, actualChannels)
}

func test_twelveChannelUnknownSpatialWithDashInName() {
let actualChannels = HLSChannels(string: twelveChannelUnknownSpatialWithDashInName)
let expectedChannels = HLSChannels(
count: 12,
spatialAudioCodingIdentifiers: ["VERY-SPATIAL"],
specialUsageIdentifiers: []
)
XCTAssertEqual(expectedChannels, actualChannels)
}

func test_sixChannelNoSpatialWithDownmix() {
let actualChannels = HLSChannels(string: sixChannelNoSpatialWithDownmix)
let expectedChannels = HLSChannels(
count: 6,
spatialAudioCodingIdentifiers: [],
specialUsageIdentifiers: [.downmix]
)
XCTAssertEqual(expectedChannels, actualChannels)
}

func test_sixChannelNoSpatialWithBinauralAndImmersive() {
let actualChannels = HLSChannels(string: sixChannelNoSpatialWithBinauralAndImmersive)
let expectedChannels = HLSChannels(
count: 6,
spatialAudioCodingIdentifiers: [],
specialUsageIdentifiers: [.binaural, .immersive]
)
XCTAssertEqual(expectedChannels, actualChannels)
}

func test_twelveChannelJocAndImmersive() {
let actualChannels = HLSChannels(string: twelveChannelJocAndImmersive)
let expectedChannels = HLSChannels(
count: 12,
spatialAudioCodingIdentifiers: ["JOC"],
specialUsageIdentifiers: [.immersive]
)
XCTAssertEqual(expectedChannels, actualChannels)
}

func test_sixChannelUnknownSpecialUsageIdentifier() {
let actualChannels = HLSChannels(string: sixChannelUnknownSpecialUsageIdentifier)
let expectedChannels = HLSChannels(
count: 6,
spatialAudioCodingIdentifiers: [],
specialUsageIdentifiers: [.unrecognized("NEW-IDENTIFIER")]
)
XCTAssertEqual(expectedChannels, actualChannels)
}
}
98 changes: 98 additions & 0 deletions mambaTests/Util Tests/Value Types/HLSVideoLayoutTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//
// HLSVideoLayoutTests.swift
// mamba
//
// Created by Robert Galluccio on 9/2/24.
// Copyright © 2024 Comcast Corporation.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License. All rights reserved.
//

import Foundation
import XCTest
import mamba

class HLSVideoLayoutTests: XCTestCase {
let empty = ""
let 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(HLSVideoLayout(string: empty))
}

func test_unrecognizedVideoLayout() {
guard let videoLayout = HLSVideoLayout(string: unrecognizedVideoLayout) else {
return XCTFail("Expected to parse REQ-VIDEO-LAYOUT from \(unrecognizedVideoLayout).")
}
XCTAssertEqual(videoLayout.layouts, [.unrecognized(unrecognizedVideoLayout)])
XCTAssertEqual(videoLayout.predominantLayout, .unrecognized(unrecognizedVideoLayout))
XCTAssertFalse(videoLayout.contains(.chMono))
XCTAssertFalse(videoLayout.contains(.chStereo))
XCTAssertTrue(videoLayout.contains(.unrecognized(unrecognizedVideoLayout)))
}

func test_monoLayout() {
guard let videoLayout = HLSVideoLayout(string: monoLayout) else {
return XCTFail("Expected to parse REQ-VIDEO-LAYOUT from \(monoLayout).")
}
XCTAssertEqual(videoLayout.layouts, [.chMono])
XCTAssertEqual(videoLayout.predominantLayout, .chMono)
XCTAssertTrue(videoLayout.contains(.chMono))
XCTAssertFalse(videoLayout.contains(.chStereo))
}

func test_stereoLayout() {
guard let videoLayout = HLSVideoLayout(string: stereoLayout) else {
return XCTFail("Expected to parse REQ-VIDEO-LAYOUT from \(stereoLayout).")
}
XCTAssertEqual(videoLayout.layouts, [.chStereo])
XCTAssertEqual(videoLayout.predominantLayout, .chStereo)
XCTAssertFalse(videoLayout.contains(.chMono))
XCTAssertTrue(videoLayout.contains(.chStereo))
}

func test_stereoWithMonoLayout() {
guard let videoLayout = HLSVideoLayout(string: stereoWithMonoLayout) else {
return XCTFail("Expected to parse REQ-VIDEO-LAYOUT from \(stereoWithMonoLayout).")
}
XCTAssertEqual(videoLayout.layouts, [.chStereo, .chMono])
XCTAssertEqual(videoLayout.predominantLayout, .chStereo)
XCTAssertTrue(videoLayout.contains(.chMono))
XCTAssertTrue(videoLayout.contains(.chStereo))
}

func test_monoWithStereoLayout() {
guard let videoLayout = HLSVideoLayout(string: monoWithStereoLayout) else {
return XCTFail("Expected to parse REQ-VIDEO-LAYOUT from \(monoWithStereoLayout).")
}
XCTAssertEqual(videoLayout.layouts, [.chMono, .chStereo])
XCTAssertEqual(videoLayout.predominantLayout, .chMono)
XCTAssertTrue(videoLayout.contains(.chMono))
XCTAssertTrue(videoLayout.contains(.chStereo))
}

func test_monoWithStereoWithUnrecognizedLayout() {
guard let videoLayout = HLSVideoLayout(string: monoWithStereoWithUnrecognizedLayout) else {
return XCTFail("Expected to parse REQ-VIDEO-LAYOUT from \(monoWithStereoWithUnrecognizedLayout).")
}
XCTAssertEqual(videoLayout.layouts, [.chMono, .chStereo, .unrecognized("CH-TRI")])
XCTAssertEqual(videoLayout.predominantLayout, .chMono)
XCTAssertTrue(videoLayout.contains(.chMono))
XCTAssertTrue(videoLayout.contains(.chStereo))
XCTAssertTrue(videoLayout.contains(.unrecognized("CH-TRI")))
}
}