Skip to content

Commit 1d88220

Browse files
committed
Fix regression in live playlist updates containing discontinuities by missing DISCONTINUITY-SEQUENCE
Fixes #7163
1 parent a24c867 commit 1d88220

File tree

2 files changed

+160
-12
lines changed

2 files changed

+160
-12
lines changed

src/utils/level-helper.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,15 @@ export function mergeDetails(
168168
oldDetails,
169169
newDetails,
170170
(oldFrag, newFrag, newFragIndex, newFragments) => {
171-
if (newDetails.skippedSegments) {
172-
if (newFrag.cc !== oldFrag.cc) {
173-
const ccOffset = oldFrag.cc - newFrag.cc;
174-
for (let i = newFragIndex; i < newFragments.length; i++) {
175-
newFragments[i].cc += ccOffset;
176-
}
171+
if (!newDetails.startCC && newFrag.cc !== oldFrag.cc) {
172+
const ccOffset = oldFrag.cc - newFrag.cc;
173+
for (let i = newFragIndex; i < newFragments.length; i++) {
174+
newFragments[i].cc += ccOffset;
177175
}
176+
newDetails.startCC =
177+
getFragmentWithSN(oldDetails, newDetails.startSN - 1)?.cc ??
178+
newFragments[0].cc;
179+
newDetails.endCC = newFragments[newFragments.length - 1].cc;
178180
}
179181
if (
180182
Number.isFinite(oldFrag.startPTS) &&
@@ -523,7 +525,7 @@ export function computeReloadInterval(
523525
export function getFragmentWithSN(
524526
details: LevelDetails | undefined,
525527
sn: number,
526-
fragCurrent: Fragment | null,
528+
fragCurrent?: Fragment | null,
527529
): MediaFragment | null {
528530
if (!details) {
529531
return null;

tests/unit/controller/level-helper.ts

Lines changed: 151 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ const getIteratedSequence = (oldPlaylist, newPlaylist) => {
6666
return actual;
6767
};
6868

69+
const getFragmentSequenceNumbers = (details: LevelDetails) =>
70+
details.fragments.map((f) => `${f?.sn}-${f?.cc}`).join(',');
71+
6972
describe('LevelHelper Tests', function () {
7073
let sandbox;
7174
beforeEach(function () {
@@ -180,9 +183,6 @@ describe('LevelHelper Tests', function () {
180183
});
181184

182185
describe('mergeDetails', function () {
183-
const getFragmentSequenceNumbers = (details: LevelDetails) =>
184-
details.fragments.map((f) => `${f?.sn}-${f?.cc}`).join(',');
185-
186186
it('transfers start times where segments overlap, and extrapolates the start of any new segment', function () {
187187
const oldPlaylist = generatePlaylist([1, 2, 3, 4]); // start times: 0, 5, 10, 15
188188
const newPlaylist = generatePlaylist([2, 3, 4, 5]);
@@ -704,6 +704,7 @@ fileSequence7.m4s
704704
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=3.0
705705
#EXT-X-MEDIA-SEQUENCE:102
706706
#EXT-X-DISCONTINUITY-SEQUENCE:10
707+
#EXT-X-DISCONTINUITY
707708
#EXT-X-SKIP:SKIPPED-SEGMENTS=4
708709
#EXTINF:6,
709710
fileSequence6.m4s
@@ -768,8 +769,8 @@ fileSequence8.m4s
768769
endSN: 108,
769770
lastPartSn: 109,
770771
lastPartIndex: 0,
771-
startCC: 10, // w/o disco-sequence incremented
772-
endCC: 11, // end CC reflects delta details until merged with previous
772+
startCC: 10,
773+
endCC: 12,
773774
});
774775
expect(
775776
details2.fragments,
@@ -979,6 +980,151 @@ fileSequence6.ts`;
979980
mergeDetails(details, details);
980981
expect(details.fragmentStart).to.equal(10);
981982
});
983+
984+
it('aligns discontinuities based on media sequence number', function () {
985+
const playlist1 = `#EXTM3U
986+
#EXT-X-VERSION:6
987+
#EXT-X-TARGETDURATION:2
988+
#EXT-X-MEDIA-SEQUENCE:26
989+
#EXT-X-MAP:URI="video_init.mp4"
990+
#EXTINF:2.000,
991+
video_26.m4s
992+
#EXT-X-DISCONTINUITY
993+
#EXTINF:2.000,
994+
video_27.m4s
995+
#EXT-X-DISCONTINUITY
996+
#EXTINF:2.000,
997+
video_28.m4s
998+
#EXT-X-DISCONTINUITY
999+
#EXTINF:2.000,
1000+
video_29.m4s
1001+
#EXTINF:2.000,
1002+
video_30.m4s
1003+
#EXT-X-DISCONTINUITY
1004+
#EXTINF:2.000,
1005+
video_31.m4s`;
1006+
const playlist2 = `#EXTM3U
1007+
#EXT-X-VERSION:6
1008+
#EXT-X-TARGETDURATION:2
1009+
#EXT-X-MEDIA-SEQUENCE:28
1010+
#EXT-X-MAP:URI="video_init.mp4"
1011+
#EXTINF:2.000,
1012+
video_28.m4s
1013+
#EXT-X-DISCONTINUITY
1014+
#EXTINF:2.000,
1015+
video_29.m4s
1016+
#EXTINF:2.000,
1017+
video_30.m4s
1018+
#EXT-X-DISCONTINUITY
1019+
#EXTINF:2.000,
1020+
video_31.m4s
1021+
#EXT-X-DISCONTINUITY
1022+
#EXTINF:2.000,
1023+
video_32.m4s`;
1024+
const oldPlaylist = parseLevelPlaylist(playlist1);
1025+
const newPlaylist = parseLevelPlaylist(playlist2);
1026+
1027+
expect(oldPlaylist).to.include({
1028+
startSN: 26,
1029+
startCC: 0,
1030+
endCC: 4,
1031+
});
1032+
expect(newPlaylist).to.include({
1033+
startSN: 28,
1034+
startCC: 0,
1035+
endCC: 3,
1036+
});
1037+
1038+
mergeDetails(oldPlaylist, newPlaylist);
1039+
1040+
expect(newPlaylist.playlistParsingError).to.be.null;
1041+
expect(newPlaylist).to.include({
1042+
startSN: 28,
1043+
startCC: 1,
1044+
endCC: 5,
1045+
});
1046+
1047+
const mergedSequence = getFragmentSequenceNumbers(newPlaylist);
1048+
expect(mergedSequence).to.equal('28-2,29-3,30-3,31-4,32-5');
1049+
});
1050+
1051+
it('aligns discontinuities based on media sequence number (#7163)', function () {
1052+
const playlist1 = `#EXTM3U
1053+
#EXT-X-VERSION:7
1054+
#EXT-X-TARGETDURATION:2
1055+
#EXT-X-MEDIA-SEQUENCE:0
1056+
#EXT-X-DISCONTINUITY
1057+
#EXT-X-MAP:URI="getMP4InitFragment.mp4"
1058+
#EXT-X-PROGRAM-DATE-TIME:2025-04-08T14:20:25.166Z
1059+
#EXTINF:1.875,
1060+
getMP4MediaFragment.mp4?FragmentNumber=91343852333398821492057832144523283530704043374
1061+
#EXT-X-DISCONTINUITY
1062+
#EXT-X-PROGRAM-DATE-TIME:2025-04-08T14:20:27.155Z
1063+
#EXTINF:1.73,
1064+
getMP4MediaFragment.mp4?FragmentNumber=91343852333398821497009592301664805164162363767
1065+
#EXT-X-DISCONTINUITY
1066+
#EXT-X-PROGRAM-DATE-TIME:2025-04-08T14:20:28.696Z
1067+
#EXTINF:1.698,
1068+
getMP4MediaFragment.mp4?FragmentNumber=91343852333398821501961352458806326677392019605
1069+
#EXT-X-DISCONTINUITY
1070+
#EXT-X-PROGRAM-DATE-TIME:2025-04-08T14:20:30.387Z
1071+
#EXTINF:1.742,
1072+
getMP4MediaFragment.mp4?FragmentNumber=91343852333398821506913112615947848230969811191
1073+
#EXT-X-DISCONTINUITY
1074+
#EXT-X-PROGRAM-DATE-TIME:2025-04-08T14:20:32.335Z
1075+
#EXTINF:1.685,
1076+
getMP4MediaFragment.mp4?FragmentNumber=91343852333398821511864872773089369853347903342`;
1077+
const playlist2 = `#EXTM3U
1078+
#EXT-X-VERSION:7
1079+
#EXT-X-TARGETDURATION:2
1080+
#EXT-X-MEDIA-SEQUENCE:1
1081+
#EXT-X-DISCONTINUITY
1082+
#EXT-X-MAP:URI="getMP4InitFragment.mp4"
1083+
#EXT-X-PROGRAM-DATE-TIME:2025-04-08T14:20:27.155Z
1084+
#EXTINF:1.73,
1085+
getMP4MediaFragment.mp4?FragmentNumber=91343852333398821497009592301664805164162363767
1086+
#EXT-X-DISCONTINUITY
1087+
#EXT-X-PROGRAM-DATE-TIME:2025-04-08T14:20:28.696Z
1088+
#EXTINF:1.698,
1089+
getMP4MediaFragment.mp4?FragmentNumber=91343852333398821501961352458806326677392019605
1090+
#EXT-X-DISCONTINUITY
1091+
#EXT-X-PROGRAM-DATE-TIME:2025-04-08T14:20:30.387Z
1092+
#EXTINF:1.742,
1093+
getMP4MediaFragment.mp4?FragmentNumber=91343852333398821506913112615947848230969811191
1094+
#EXT-X-DISCONTINUITY
1095+
#EXT-X-PROGRAM-DATE-TIME:2025-04-08T14:20:32.335Z
1096+
#EXTINF:1.685,
1097+
getMP4MediaFragment.mp4?FragmentNumber=91343852333398821511864872773089369853347903342
1098+
#EXT-X-DISCONTINUITY
1099+
#EXT-X-PROGRAM-DATE-TIME:2025-04-08T14:20:33.806Z
1100+
#EXTINF:1.677,
1101+
getMP4MediaFragment.mp4?FragmentNumber=91343852333398821516816632930230891347979802607`;
1102+
const oldPlaylist = parseLevelPlaylist(playlist1);
1103+
const newPlaylist = parseLevelPlaylist(playlist2);
1104+
1105+
expect(oldPlaylist).to.include({
1106+
startSN: 0,
1107+
startCC: 0,
1108+
endCC: 5,
1109+
});
1110+
expect(newPlaylist).to.include({
1111+
startSN: 1,
1112+
startCC: 0,
1113+
endCC: 5,
1114+
});
1115+
1116+
mergeDetails(oldPlaylist, newPlaylist);
1117+
1118+
expect(newPlaylist.playlistParsingError).to.be.null;
1119+
expect(newPlaylist).to.include({
1120+
startSN: 1,
1121+
startCC: 1,
1122+
endCC: 6,
1123+
});
1124+
1125+
const mergedSequence = getFragmentSequenceNumbers(newPlaylist);
1126+
expect(mergedSequence).to.equal('1-2,2-3,3-4,4-5,5-6');
1127+
});
9821128
});
9831129

9841130
describe('computeReloadInterval', function () {

0 commit comments

Comments
 (0)