Skip to content

Commit fe6e958

Browse files
committed
Fix delta playlist update discontinuity regression introduced in v1.6.2 with #7119 and #7168
Fixes #7282
1 parent c57e450 commit fe6e958

File tree

3 files changed

+175
-6
lines changed

3 files changed

+175
-6
lines changed

src/loader/m3u8-parser.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,9 @@ export function mapDateRanges(
758758
) {
759759
// Make sure DateRanges are mapped to a ProgramDateTime tag that applies a date to a segment that overlaps with its start date
760760
const programDateTimeCount = programDateTimes.length;
761+
if (!programDateTimeCount) {
762+
return;
763+
}
761764
const lastProgramDateTime = programDateTimes[programDateTimeCount - 1];
762765
const playlistEnd = details.live ? Infinity : details.totalduration;
763766
const dateRangeIds = Object.keys(details.dateRanges);

src/utils/level-helper.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,14 @@ export function mergeDetails(
168168
oldDetails,
169169
newDetails,
170170
(oldFrag, newFrag, newFragIndex, newFragments) => {
171-
if (!newDetails.startCC && newFrag.cc !== oldFrag.cc) {
171+
if (
172+
(!newDetails.startCC || newDetails.skippedSegments) &&
173+
newFrag.cc !== oldFrag.cc
174+
) {
172175
const ccOffset = oldFrag.cc - newFrag.cc;
173176
for (let i = newFragIndex; i < newFragments.length; i++) {
174177
newFragments[i].cc += ccOffset;
175178
}
176-
newDetails.startCC =
177-
getFragmentWithSN(oldDetails, newDetails.startSN - 1)?.cc ??
178-
newFragments[0].cc;
179179
newDetails.endCC = newFragments[newFragments.length - 1].cc;
180180
}
181181
if (
@@ -243,7 +243,6 @@ export function mergeDetails(
243243
}
244244
newDetails.startSN = newFragments[0].sn;
245245
} else {
246-
newDetails.endCC = newFragments[newFragments.length - 1].cc;
247246
if (newDetails.canSkipDateRanges) {
248247
newDetails.dateRanges = mergeDateRanges(
249248
oldDetails.dateRanges,
@@ -266,6 +265,14 @@ export function mergeDetails(
266265
}
267266
mapDateRanges(programDateTimes, newDetails);
268267
}
268+
newDetails.endCC = newFragments[newFragments.length - 1].cc;
269+
}
270+
if (!newDetails.startCC) {
271+
const fragPriorToNewStart = getFragmentWithSN(
272+
oldDetails,
273+
newDetails.startSN - 1,
274+
);
275+
newDetails.startCC = fragPriorToNewStart?.cc ?? newFragments[0].cc;
269276
}
270277

271278
// Merge parts

tests/unit/controller/level-helper.ts

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,7 @@ fileSequence8.m4s
818818
startCC: 10, // w/o disco-sequence incremented
819819
endCC: 11,
820820
});
821+
expect(details2.fragments[0]).to.include({ sn: 102, cc: 11 });
821822
expect(details2.fragmentHint).to.include({ sn: 109, cc: 12 });
822823
expect(mergedSequence1).to.equal(
823824
'102-11,103-11,104-11,105-11,106-11,107-11,108-11',
@@ -845,13 +846,171 @@ fileSequence8.m4s
845846
startCC: 11, // w/ disco-sequence incremented
846847
endCC: 11,
847848
});
848-
expect(details2.fragmentHint).to.include({ sn: 109, cc: 12 });
849+
expect(details3.fragments[0]).to.include({ sn: 102, cc: 11 });
850+
expect(details3.fragmentHint).to.include({ sn: 109, cc: 12 });
849851
expect(mergedSequence2).to.equal(
850852
'102-11,103-11,104-11,105-11,106-11,107-11,108-11',
851853
);
852854
expect(details3.playlistParsingError).to.be.null;
853855
});
854856

857+
it('handles delta Playlist updates with discontinuities and parts and no sequence change (missing PDT)', function () {
858+
const playlist = `#EXTM3U
859+
#EXT-X-VERSION:10
860+
#EXT-X-INDEPENDENT-SEGMENTS
861+
#EXT-X-TARGETDURATION:3
862+
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=3.126000,CAN-SKIP-UNTIL=20.0
863+
#EXT-X-PART-INF:PART-TARGET=1.000000
864+
#EXT-X-MEDIA-SEQUENCE:0
865+
#EXT-X-DISCONTINUITY-SEQUENCE:0
866+
#EXT-X-DATERANGE:ID="nmss-daterange",START-DATE="2025-05-22T07:59:38.809Z"
867+
868+
#EXT-X-MAP:URI="720p_0_0.m4s"
869+
#EXTINF:2.500000,
870+
720p_0.m4v
871+
#EXTINF:2.500000,
872+
720p_1.m4v
873+
#EXTINF:2.500000,
874+
720p_2.m4v
875+
#EXTINF:2.184667,
876+
#EXT-X-DISCONTINUITY
877+
#EXT-X-MAP:URI="720p_0_1.m4s
878+
#EXTINF:2.500000,
879+
720p_3.m4v
880+
#EXTINF:2.500000,
881+
720p_4.m4v
882+
#EXTINF:2.500000,
883+
720p_5.m4v
884+
#EXTINF:2.500000,
885+
720p_6.m4v
886+
#EXTINF:2.500000,
887+
720p_7.m4v
888+
#EXTINF:2.500000,
889+
720p_8.m4v
890+
#EXT-X-PART:DURATION=1.000000,URI="720p_9_0.m4v",INDEPENDENT=YES
891+
#EXT-X-PART:DURATION=1.000000,URI="720p_9_1.m4v"
892+
#EXT-X-PART:DURATION=0.500000,URI="720p_9_2.m4v"
893+
#EXTINF:2.500000,
894+
720p_9.m4v
895+
#EXT-X-PART:DURATION=1.000000,URI="720p_10_0.m4v",INDEPENDENT=YES
896+
#EXT-X-PART:DURATION=1.000000,URI="720p_10_1.m4v"
897+
#EXT-X-PART:DURATION=0.500000,URI="720p_10_2.m4v"
898+
#EXTINF:2.500000,
899+
720p_10.m4v
900+
#EXT-X-PART:DURATION=1.000000,URI="720p_11_0.m4v",INDEPENDENT=YES
901+
#EXT-X-PART:DURATION=1.000000,URI="720p_11_1.m4v"
902+
#EXT-X-PART:DURATION=0.500000,URI="720p_11_2.m4v"
903+
#EXTINF:2.500000,
904+
720p_11.m4v
905+
#EXT-X-PART:DURATION=1.000000,URI="720p_12_0.m4v",INDEPENDENT=YES
906+
#EXT-X-PART:DURATION=1.000000,URI="720p_12_0.m4v"
907+
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="720p_12_2.m4v"`;
908+
const deltaUpdate = `#EXTM3U
909+
#EXT-X-VERSION:10
910+
#EXT-X-INDEPENDENT-SEGMENTS
911+
#EXT-X-TARGETDURATION:3
912+
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=3.126000,CAN-SKIP-UNTIL=20.0
913+
#EXT-X-PART-INF:PART-TARGET=1.000000
914+
#EXT-X-MEDIA-SEQUENCE:0
915+
#EXT-X-DISCONTINUITY-SEQUENCE:0
916+
#EXT-X-DATERANGE:ID="nmss-daterange",START-DATE="2025-05-22T07:59:38.809Z"
917+
#EXT-X-SKIP:SKIPPED-SEGMENTS=4
918+
919+
#EXT-X-MAP:URI="720p_0_1.m4s"
920+
720p_4.m4v
921+
#EXTINF:2.500000,
922+
720p_5.m4v
923+
#EXTINF:2.500000,
924+
720p_6.m4v
925+
#EXTINF:2.500000,
926+
720p_7.m4v
927+
#EXTINF:2.500000,
928+
720p_8.m4v
929+
#EXT-X-PART:DURATION=1.000000,URI="720p_9_0.m4v",INDEPENDENT=YES
930+
#EXT-X-PART:DURATION=1.000000,URI="720p_9_1.m4v"
931+
#EXT-X-PART:DURATION=0.500000,URI="720p_9_2.m4v"
932+
#EXTINF:2.500000,
933+
720p_9.m4v
934+
#EXT-X-PART:DURATION=1.000000,URI="720p_10_0.m4v",INDEPENDENT=YES
935+
#EXT-X-PART:DURATION=1.000000,URI="720p_10_1.m4v"
936+
#EXT-X-PART:DURATION=0.500000,URI="720p_10_2.m4v"
937+
#EXTINF:2.500000,
938+
720p_10.m4v
939+
#EXT-X-PART:DURATION=1.000000,URI="720p_11_0.m4v",INDEPENDENT=YES
940+
#EXT-X-PART:DURATION=1.000000,URI="720p_11_1.m4v"
941+
#EXT-X-PART:DURATION=0.500000,URI="720p_11_2.m4v"
942+
#EXTINF:2.500000,
943+
720p_11.m4v
944+
#EXT-X-PART:DURATION=1.000000,URI="720p_12_0.m4v",INDEPENDENT=YES
945+
#EXT-X-PART:DURATION=1.000000,URI="720p_12_0.m4v"
946+
#EXT-X-PART:DURATION=1.000000,URI="720p_12_0.m4v"
947+
#EXTINF:2.500000,
948+
720p_12.m4v
949+
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="720p_13_0.m4`;
950+
951+
const details1 = parseLevelPlaylist(playlist);
952+
const details2 = parseLevelPlaylist(deltaUpdate);
953+
expect(details1, 'details1').to.include({
954+
live: true,
955+
canSkipUntil: 20,
956+
totalduration: 32,
957+
startSN: 0,
958+
endSN: 11,
959+
fragmentStart: 0,
960+
lastPartSn: 12,
961+
lastPartIndex: 1,
962+
startCC: 0,
963+
endCC: 1,
964+
});
965+
966+
expect(
967+
details2,
968+
'delta w/o disco seq incremented before merging',
969+
).to.include({
970+
live: true,
971+
skippedSegments: 4,
972+
canSkipUntil: 20,
973+
totalduration: 32,
974+
startSN: 0,
975+
endSN: 12,
976+
lastPartSn: 12,
977+
lastPartIndex: 2,
978+
startCC: 0,
979+
endCC: 0, // endCC of delta playlist will not be incremented until merged
980+
});
981+
expect(
982+
details2.fragments,
983+
'delta w/o disco seq incremented fragments',
984+
).to.have.lengthOf(13);
985+
986+
// This delta update does not increment discontinuity-sequence (discontinuity tag would appear before first segment)
987+
details2.reloaded(details1);
988+
expect(details2, 'delta w/o disco seq incremented reloaded').to.include({
989+
misses: 0,
990+
advanced: true,
991+
updated: true,
992+
});
993+
// discontinuity sequence numbers (frag.cc) should be carried over
994+
mergeDetails(details1, details2);
995+
const mergedSequence1 = getFragmentSequenceNumbers(details2);
996+
expect(
997+
details2,
998+
`delta w/o disco seq incremented merged (${mergedSequence1})`,
999+
).to.include({
1000+
skippedSegments: 4,
1001+
deltaUpdateFailed: false,
1002+
startSN: 0,
1003+
endSN: 12,
1004+
startCC: 0,
1005+
endCC: 1, // incremented after merging
1006+
});
1007+
expect(details2.fragmentHint).to.include({ sn: 13, cc: 1 });
1008+
expect(mergedSequence1).to.equal(
1009+
'0-0,1-0,2-0,3-1,4-1,5-1,6-1,7-1,8-1,9-1,10-1,11-1,12-1',
1010+
);
1011+
expect(details2.playlistParsingError).to.be.null;
1012+
});
1013+
8551014
it('handles delta Playlist updates with multiple skip tags and removed date ranges', function () {
8561015
const playlist = `#EXTM3U
8571016
#EXT-X-TARGETDURATION:6

0 commit comments

Comments
 (0)