diff --git a/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift b/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift index b433e5f..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 @@ -508,6 +515,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 +558,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 +573,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/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") + } }