From 35c032d7f9dbbeb96d2b79183374a6822e5805ea Mon Sep 17 00:00:00 2001 From: Gaurang Bham Date: Wed, 3 Sep 2025 15:03:57 -0400 Subject: [PATCH 1/2] Fresh branch to prevent build failures --- .../HLSPlaylistStructure.swift | 20 ++++++-- mambaTests/HLSPlaylistStructureTests.swift | 48 +++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift b/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift index b433e5f..a71f627 100644 --- a/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift +++ b/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift @@ -508,6 +508,11 @@ fileprivate struct HLSPlaylistStructureConstructor { header: TagGroup?, mediaSegmentGroups: [MediaSegmentTagGroup]) throws -> [TagSpan] { + // If the playlist contains no segments then there are no spans + if mediaSegmentGroups.isEmpty { + return [] + } + var mediaSpans = [TagSpan]() // handle our only known spannable tag, `EXT-X-KEY` @@ -546,7 +551,9 @@ fileprivate struct HLSPlaylistStructureConstructor { if let startKeyIndex = startKeyIndex, let startKeyTag = startKeyTag { // we are closing out our last key - mediaSpans.append(TagSpan(parentTag: startKeyTag, tagMediaSpan: startKeyIndex...currentIndex - 1)) + if startKeyIndex <= currentIndex - 1 { + mediaSpans.append(TagSpan(parentTag: startKeyTag, tagMediaSpan: startKeyIndex...(currentIndex - 1))) + } } startKeyIndex = currentIndex @@ -559,10 +566,15 @@ fileprivate struct HLSPlaylistStructureConstructor { // close out our last tag if let startKeyIndex = startKeyIndex, let startKeyTag = startKeyTag { - mediaSpans.append(TagSpan(parentTag: startKeyTag, tagMediaSpan: startKeyIndex...(currentIndex - 1))) + if startKeyIndex <= currentIndex - 1 { + mediaSpans.append(TagSpan(parentTag: startKeyTag, tagMediaSpan: startKeyIndex...(currentIndex - 1))) + } + } + + // instead of assert, warn softly if key counts mismatch (footer keys or malformed playlists) + if keyCount != keyTags.count { + print("Warning: generateMediaSpans counted \(keyCount) EXT-X-KEY tags, but found \(keyTags.count). Possibly due to footer-only key tags.") } - - assert(keyCount == keyTags.count, "we missed a key tag") return mediaSpans } diff --git a/mambaTests/HLSPlaylistStructureTests.swift b/mambaTests/HLSPlaylistStructureTests.swift index dda34b1..9313028 100644 --- a/mambaTests/HLSPlaylistStructureTests.swift +++ b/mambaTests/HLSPlaylistStructureTests.swift @@ -82,6 +82,54 @@ class HLSPlaylistStructureTests: XCTestCase { XCTAssert(structure.mediaSegmentGroups.count == 0) XCTAssertNil(structure.header) } + + // This validates the early return logic in generateMediaSpans() for empty mediaSegmentGroups. + func testNoMediaSegmentsScenario() { + print("Starting test: testNoMediaSegmentsScenario") + let hlsArray = [ + "#EXTM3U\n", + "#EXT-X-TARGETDURATION:6\n", + "#EXT-X-VERSION:3\n", + "#EXT-X-MEDIA-SEQUENCE:0\n", + "#EXT-X-PLAYLIST-TYPE:VOD\n", + "#EXT-X-KEY:METHOD=NONE\n", + "#EXT-X-MAP:URI=\"test.mp4\",BYTERANGE=\"610@0\"\n" + ] + let hlsString = hlsArray.joined() + runTest(hlsString: hlsString, expectedSpans: []) + print("Finished test: testNoMediaSegmentsScenario") + } + + // Covers an edge case crash where an EXT-X-KEY appears in the header, but the playlist has no media segments. + func testKeyInHeaderWithNoMediaSegmentsDoesNotCrash() { + print("Starting test: testKeyInHeaderWithNoMediaSegmentsDoesNotCrash") + let hlsArray = [ + "#EXTM3U\n", + "#EXT-X-VERSION:3\n", + "#EXT-X-KEY:METHOD=AES-128,URI=\"enc.key\"\n", + "#EXT-X-ENDLIST\n" + ] + let hlsString = hlsArray.joined() + runTest(hlsString: hlsString, expectedSpans: []) + print("Finished test: testKeyInHeaderWithNoMediaSegmentsDoesNotCrash") + } + + // Validates that an EXT-X-KEY tag appearing after the last media segment (in the footer) does not crash generateMediaSpans() or create invalid spans. This seems to occur with DAI + func testFooterOnlyKeyDoesNotCrashOrAppend() { + print("Starting test: testFooterOnlyKeyDoesNotCrashOrAppend") + let hlsArray = [ + "#EXTM3U\n", + "#EXT-X-VERSION:3\n", + "#EXT-X-TARGETDURATION:6\n", + "#EXTINF:6.0,\n", + "segment1.ts\n", + "#EXT-X-KEY:METHOD=AES-128,URI=\"footer.key\"\n", + "#EXT-X-ENDLIST\n" + ] + let hlsString = hlsArray.joined() + runTest(hlsString: hlsString, expectedSpans: []) + print("Finished test: testFooterOnlyKeyDoesNotCrashOrAppend") + } } // unusual/malformed playlist to test footer deletion From 0efc398387fd8f3e6d444f6ea5877c60e86a5ba4 Mon Sep 17 00:00:00 2001 From: Gaurang Bham Date: Thu, 4 Sep 2025 14:25:24 -0400 Subject: [PATCH 2/2] Partially addressed Robert's feedback --- .../HLSPlaylistStructure.swift | 13 +++-- mambaTests/HLSMediaSpanTests.swift | 48 +++++++++++++++++++ mambaTests/HLSPlaylistStructureTests.swift | 48 ------------------- 3 files changed, 58 insertions(+), 51 deletions(-) diff --git a/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift b/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift index a71f627..462600b 100644 --- a/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift +++ b/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift @@ -244,9 +244,16 @@ final class HLSPlaylistStructure: HLSPlaylistStructureInterface { do { let result = try HLSPlaylistStructureConstructor.generateMediaGroups(fromTags: _tags) - let mediaSpans = try HLSPlaylistStructureConstructor.generateMediaSpans(fromTags: _tags, - header: result.header, - mediaSegmentGroups: result.mediaSegmentGroups) + let mediaSpans: [TagSpan] + do { + mediaSpans = try HLSPlaylistStructureConstructor.generateMediaSpans( + fromTags: _tags, + header: result.header, + mediaSegmentGroups: result.mediaSegmentGroups + ) + } catch { + mediaSpans = [] + } self._header = result.header self._mediaSegmentGroups = result.mediaSegmentGroups diff --git a/mambaTests/HLSMediaSpanTests.swift b/mambaTests/HLSMediaSpanTests.swift index 3de33f9..16ea124 100644 --- a/mambaTests/HLSMediaSpanTests.swift +++ b/mambaTests/HLSMediaSpanTests.swift @@ -114,4 +114,52 @@ class HLSMediaSpanTests: XCTestCase { let hlsString = hlsArray.joined() runTest(hlsString: hlsString, expectedSpans: [0...1, 2...4, 5...8]) } + + // This validates the early return logic in generateMediaSpans() for empty mediaSegmentGroups. + func testNoMediaSegmentsScenario() { + print("Starting test: testNoMediaSegmentsScenario") + let hlsArray = [ + "#EXTM3U\n", + "#EXT-X-TARGETDURATION:6\n", + "#EXT-X-VERSION:3\n", + "#EXT-X-MEDIA-SEQUENCE:0\n", + "#EXT-X-PLAYLIST-TYPE:VOD\n", + "#EXT-X-KEY:METHOD=NONE\n", + "#EXT-X-MAP:URI=\"test.mp4\",BYTERANGE=\"610@0\"\n" + ] + let hlsString = hlsArray.joined() + runTest(hlsString: hlsString, expectedSpans: []) + print("Finished test: testNoMediaSegmentsScenario") + } + + // Covers an edge case crash where an EXT-X-KEY appears in the header, but the playlist has no media segments. + func testKeyInHeaderWithNoMediaSegmentsDoesNotCrash() { + print("Starting test: testKeyInHeaderWithNoMediaSegmentsDoesNotCrash") + let hlsArray = [ + "#EXTM3U\n", + "#EXT-X-VERSION:3\n", + "#EXT-X-KEY:METHOD=AES-128,URI=\"enc.key\"\n", + "#EXT-X-ENDLIST\n" + ] + let hlsString = hlsArray.joined() + runTest(hlsString: hlsString, expectedSpans: []) + print("Finished test: testKeyInHeaderWithNoMediaSegmentsDoesNotCrash") + } + + // Validates that an EXT-X-KEY tag appearing after the last media segment (in the footer) does not crash generateMediaSpans() or create invalid spans. This seems to occur with DAI + func testFooterOnlyKeyDoesNotCrashOrAppend() { + print("Starting test: testFooterOnlyKeyDoesNotCrashOrAppend") + let hlsArray = [ + "#EXTM3U\n", + "#EXT-X-VERSION:3\n", + "#EXT-X-TARGETDURATION:6\n", + "#EXTINF:6.0,\n", + "segment1.ts\n", + "#EXT-X-KEY:METHOD=AES-128,URI=\"footer.key\"\n", + "#EXT-X-ENDLIST\n" + ] + let hlsString = hlsArray.joined() + runTest(hlsString: hlsString, expectedSpans: []) + print("Finished test: testFooterOnlyKeyDoesNotCrashOrAppend") + } } diff --git a/mambaTests/HLSPlaylistStructureTests.swift b/mambaTests/HLSPlaylistStructureTests.swift index 9313028..dda34b1 100644 --- a/mambaTests/HLSPlaylistStructureTests.swift +++ b/mambaTests/HLSPlaylistStructureTests.swift @@ -82,54 +82,6 @@ class HLSPlaylistStructureTests: XCTestCase { XCTAssert(structure.mediaSegmentGroups.count == 0) XCTAssertNil(structure.header) } - - // This validates the early return logic in generateMediaSpans() for empty mediaSegmentGroups. - func testNoMediaSegmentsScenario() { - print("Starting test: testNoMediaSegmentsScenario") - let hlsArray = [ - "#EXTM3U\n", - "#EXT-X-TARGETDURATION:6\n", - "#EXT-X-VERSION:3\n", - "#EXT-X-MEDIA-SEQUENCE:0\n", - "#EXT-X-PLAYLIST-TYPE:VOD\n", - "#EXT-X-KEY:METHOD=NONE\n", - "#EXT-X-MAP:URI=\"test.mp4\",BYTERANGE=\"610@0\"\n" - ] - let hlsString = hlsArray.joined() - runTest(hlsString: hlsString, expectedSpans: []) - print("Finished test: testNoMediaSegmentsScenario") - } - - // Covers an edge case crash where an EXT-X-KEY appears in the header, but the playlist has no media segments. - func testKeyInHeaderWithNoMediaSegmentsDoesNotCrash() { - print("Starting test: testKeyInHeaderWithNoMediaSegmentsDoesNotCrash") - let hlsArray = [ - "#EXTM3U\n", - "#EXT-X-VERSION:3\n", - "#EXT-X-KEY:METHOD=AES-128,URI=\"enc.key\"\n", - "#EXT-X-ENDLIST\n" - ] - let hlsString = hlsArray.joined() - runTest(hlsString: hlsString, expectedSpans: []) - print("Finished test: testKeyInHeaderWithNoMediaSegmentsDoesNotCrash") - } - - // Validates that an EXT-X-KEY tag appearing after the last media segment (in the footer) does not crash generateMediaSpans() or create invalid spans. This seems to occur with DAI - func testFooterOnlyKeyDoesNotCrashOrAppend() { - print("Starting test: testFooterOnlyKeyDoesNotCrashOrAppend") - let hlsArray = [ - "#EXTM3U\n", - "#EXT-X-VERSION:3\n", - "#EXT-X-TARGETDURATION:6\n", - "#EXTINF:6.0,\n", - "segment1.ts\n", - "#EXT-X-KEY:METHOD=AES-128,URI=\"footer.key\"\n", - "#EXT-X-ENDLIST\n" - ] - let hlsString = hlsArray.joined() - runTest(hlsString: hlsString, expectedSpans: []) - print("Finished test: testFooterOnlyKeyDoesNotCrashOrAppend") - } } // unusual/malformed playlist to test footer deletion