mirror of
https://github.com/video-dev/hls.js.git
synced 2026-06-16 13:34:44 +00:00
Support EXT-X-MEDIA-SEQUENCE declaration after EXT-X-SKIP (#7905)
* Support EXT-X-MEDIA-SEQUENCE declaration after EXT-X-SKIP * Use prevFrag to detect a Media Segment preceding EXT-X-MEDIA-SEQUENCE --------- Authored-by: 배홍준[Media Player Tech] <hongjun.bae@navercorp.com>
This commit is contained in:
@@ -487,10 +487,14 @@ export default class M3U8Parser {
|
||||
case 'MEDIA-SEQUENCE':
|
||||
if (level.startSN !== 0) {
|
||||
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
|
||||
} else if (fragments.length > 0) {
|
||||
} else if (prevFrag !== null) {
|
||||
// a real Media Segment (not an EXT-X-SKIP placeholder) precedes this tag
|
||||
assignMustAppearBeforeSegmentsError(level, tag, result);
|
||||
}
|
||||
currentSN = level.startSN = parseInt(value1);
|
||||
level.startSN = parseInt(value1);
|
||||
// include skipped segments so the first real fragment keeps its sequence
|
||||
// number when EXT-X-SKIP is declared before EXT-X-MEDIA-SEQUENCE
|
||||
currentSN = level.startSN + level.skippedSegments;
|
||||
break;
|
||||
case 'SKIP': {
|
||||
if (level.skippedSegments) {
|
||||
|
||||
@@ -1149,6 +1149,169 @@ fileSequence18.ts`;
|
||||
});
|
||||
});
|
||||
|
||||
it('handles delta Playlist updates when EXT-X-SKIP is declared before EXT-X-MEDIA-SEQUENCE', function () {
|
||||
const playlist = `#EXTM3U
|
||||
#EXT-X-TARGETDURATION:6
|
||||
#EXT-X-VERSION:9
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=36
|
||||
#EXT-X-MEDIA-SEQUENCE:100
|
||||
#EXTINF:6,
|
||||
fileSequence100.ts
|
||||
#EXTINF:6,
|
||||
fileSequence101.ts
|
||||
#EXTINF:6,
|
||||
fileSequence102.ts
|
||||
#EXTINF:6,
|
||||
fileSequence103.ts
|
||||
#EXTINF:6,
|
||||
fileSequence104.ts
|
||||
#EXTINF:6,
|
||||
fileSequence105.ts
|
||||
#EXTINF:6,
|
||||
fileSequence106.ts
|
||||
#EXTINF:6,
|
||||
fileSequence107.ts
|
||||
#EXTINF:6,
|
||||
fileSequence108.ts
|
||||
#EXTINF:6,
|
||||
fileSequence109.ts`;
|
||||
const deltaUpdate1 = `#EXTM3U
|
||||
#EXT-X-TARGETDURATION:6
|
||||
#EXT-X-VERSION:9
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=36
|
||||
#EXT-X-SKIP:SKIPPED-SEGMENTS=3
|
||||
#EXT-X-MEDIA-SEQUENCE:100
|
||||
#EXTINF:6,
|
||||
fileSequence103.ts
|
||||
#EXTINF:6,
|
||||
fileSequence104.ts
|
||||
#EXTINF:6,
|
||||
fileSequence105.ts
|
||||
#EXTINF:6,
|
||||
fileSequence106.ts
|
||||
#EXTINF:6,
|
||||
fileSequence107.ts
|
||||
#EXTINF:6,
|
||||
fileSequence108.ts
|
||||
#EXTINF:6,
|
||||
fileSequence109.ts
|
||||
#EXTINF:6,
|
||||
fileSequence110.ts`;
|
||||
const deltaUpdate2 = `#EXTM3U
|
||||
#EXT-X-TARGETDURATION:6
|
||||
#EXT-X-VERSION:9
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=36
|
||||
#EXT-X-SKIP:SKIPPED-SEGMENTS=3
|
||||
#EXT-X-MEDIA-SEQUENCE:102
|
||||
#EXTINF:6,
|
||||
fileSequence105.ts
|
||||
#EXTINF:6,
|
||||
fileSequence106.ts
|
||||
#EXTINF:6,
|
||||
fileSequence107.ts
|
||||
#EXTINF:6,
|
||||
fileSequence108.ts
|
||||
#EXTINF:6,
|
||||
fileSequence109.ts
|
||||
#EXTINF:6,
|
||||
fileSequence110.ts
|
||||
#EXTINF:6,
|
||||
fileSequence111.ts
|
||||
#EXTINF:6,
|
||||
fileSequence112.ts`;
|
||||
const details1 = parseLevelPlaylist(playlist);
|
||||
const details2 = parseLevelPlaylist(deltaUpdate1);
|
||||
const details3 = parseLevelPlaylist(deltaUpdate2);
|
||||
|
||||
expect(details2.playlistParsingError, 'details2 parsing error').to.be
|
||||
.null;
|
||||
expect(details2, 'details2 before merging').to.include({
|
||||
live: true,
|
||||
skippedSegments: 3,
|
||||
startSN: 100,
|
||||
endSN: 110,
|
||||
});
|
||||
expect(details2.fragments, 'details2 parsed fragments').to.have.lengthOf(
|
||||
11,
|
||||
);
|
||||
expect(details2.fragments[0]).to.equal(null);
|
||||
expect(details2.fragments[2]).to.equal(null);
|
||||
expect(details2.fragments[3]?.sn).to.equal(103);
|
||||
|
||||
mergeDetails(details1, details2, logger);
|
||||
expect(details2, 'details2 merged with details1').to.include({
|
||||
deltaUpdateFailed: false,
|
||||
startSN: 100,
|
||||
endSN: 110,
|
||||
});
|
||||
expect(details2.playlistParsingError).to.be.null;
|
||||
details2.fragments.forEach((frag, i) => {
|
||||
expect(frag, `details2.fragments[${i}]`).to.not.equal(null);
|
||||
expect(frag.sn, `details2.fragments[${i}].sn`).to.equal(100 + i);
|
||||
expect(frag.relurl, `details2.fragments[${i}].relurl`).to.equal(
|
||||
`fileSequence${100 + i}.ts`,
|
||||
);
|
||||
});
|
||||
|
||||
mergeDetails(details2, details3, logger);
|
||||
expect(details3, 'details3 merged with details2').to.include({
|
||||
deltaUpdateFailed: false,
|
||||
startSN: 102,
|
||||
endSN: 112,
|
||||
});
|
||||
expect(details3.playlistParsingError).to.be.null;
|
||||
details3.fragments.forEach((frag, i) => {
|
||||
expect(frag, `details3.fragments[${i}]`).to.not.equal(null);
|
||||
expect(frag.sn, `details3.fragments[${i}].sn`).to.equal(102 + i);
|
||||
expect(frag.relurl, `details3.fragments[${i}].relurl`).to.equal(
|
||||
`fileSequence${102 + i}.ts`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('detects media sequence mismatch on delta updates when EXT-X-SKIP is declared before EXT-X-MEDIA-SEQUENCE', function () {
|
||||
const playlist = `#EXTM3U
|
||||
#EXT-X-TARGETDURATION:6
|
||||
#EXT-X-VERSION:9
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=36
|
||||
#EXT-X-MEDIA-SEQUENCE:100
|
||||
#EXTINF:6,
|
||||
fileSequence100.ts
|
||||
#EXTINF:6,
|
||||
fileSequence101.ts
|
||||
#EXTINF:6,
|
||||
fileSequence102.ts
|
||||
#EXTINF:6,
|
||||
fileSequence103.ts
|
||||
#EXTINF:6,
|
||||
fileSequence104.ts
|
||||
#EXTINF:6,
|
||||
fileSequence105.ts`;
|
||||
// The sequence numbering of this delta update does not match the
|
||||
// previous playlist (fileSequence104 was at 104, now at 105)
|
||||
const deltaUpdate = `#EXTM3U
|
||||
#EXT-X-TARGETDURATION:6
|
||||
#EXT-X-VERSION:9
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=36
|
||||
#EXT-X-SKIP:SKIPPED-SEGMENTS=3
|
||||
#EXT-X-MEDIA-SEQUENCE:102
|
||||
#EXTINF:6,
|
||||
fileSequence104.ts
|
||||
#EXTINF:6,
|
||||
fileSequence105.ts
|
||||
#EXTINF:6,
|
||||
fileSequence106.ts`;
|
||||
const details1 = parseLevelPlaylist(playlist);
|
||||
const details2 = parseLevelPlaylist(deltaUpdate);
|
||||
|
||||
expect(details2.playlistParsingError, 'details2 parsing error').to.be
|
||||
.null;
|
||||
mergeDetails(details1, details2, logger);
|
||||
expect(details2.playlistParsingError?.message).to.match(
|
||||
/^media sequence mismatch 105:/,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not add more sliding when LevelDetails arguments are the same object', function () {
|
||||
const playlist = `#EXTM3U
|
||||
#EXT-X-TARGETDURATION:6
|
||||
|
||||
@@ -3367,6 +3367,90 @@ a{$bar}.mp4
|
||||
'#EXT-X-MEDIA-SEQUENCE must appear before the first Media Segment (#EXT-X-MEDIA-SEQUENCE:2)',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not error when EXT-X-MEDIA-SEQUENCE is declared after EXT-X-SKIP', function () {
|
||||
const level = `#EXTM3U
|
||||
#EXT-X-VERSION:10
|
||||
#EXT-X-TARGETDURATION:3
|
||||
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=3.008,CAN-SKIP-UNTIL=35.4
|
||||
#EXT-X-PART-INF:PART-TARGET=1.001
|
||||
#EXT-X-SKIP:SKIPPED-SEGMENTS=191
|
||||
#EXT-X-MEDIA-SEQUENCE:7126
|
||||
#EXT-X-MAP:URI="https://sample-host/init.m4s"
|
||||
#EXTINF:2.953,
|
||||
https://sample-host/1080p_7317.m4v`;
|
||||
const details = M3U8Parser.parseLevelPlaylist(
|
||||
level,
|
||||
'http://example.com/hls/index.m3u8',
|
||||
0,
|
||||
PlaylistLevelType.MAIN,
|
||||
0,
|
||||
null,
|
||||
);
|
||||
expect(details.playlistParsingError).to.be.null;
|
||||
expect(details.startSN).to.equal(7126);
|
||||
expect(details.skippedSegments).to.equal(191);
|
||||
// 191 skipped placeholders (sn 7126..7316), then the first real segment at sn 7317
|
||||
expect(details.fragments.length).to.equal(192);
|
||||
expect(details.fragments[0]).to.equal(null);
|
||||
expect(details.fragments[190]).to.equal(null);
|
||||
expect(details.fragments[191]?.sn).to.equal(7317);
|
||||
});
|
||||
|
||||
it('errors when a Media Segment precedes EXT-X-SKIP and EXT-X-MEDIA-SEQUENCE', function () {
|
||||
const level = `#EXTM3U
|
||||
#EXT-X-TARGETDURATION:6
|
||||
#EXT-X-VERSION:9
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=36
|
||||
#EXTINF:6,
|
||||
fileSequence102.ts
|
||||
#EXT-X-SKIP:SKIPPED-SEGMENTS=3
|
||||
#EXT-X-MEDIA-SEQUENCE:102
|
||||
#EXTINF:6,
|
||||
fileSequence106.ts`;
|
||||
const details = M3U8Parser.parseLevelPlaylist(
|
||||
level,
|
||||
'http://example.com/hls/index.m3u8',
|
||||
0,
|
||||
PlaylistLevelType.MAIN,
|
||||
0,
|
||||
null,
|
||||
);
|
||||
expectPlaylistParsingError(
|
||||
details,
|
||||
'#EXT-X-MEDIA-SEQUENCE must appear before the first Media Segment (#EXT-X-MEDIA-SEQUENCE:102)',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not error when EXT-X-SKIP replaces segments after the first Media Segment', function () {
|
||||
const level = `#EXTM3U
|
||||
#EXT-X-TARGETDURATION:6
|
||||
#EXT-X-VERSION:9
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=36
|
||||
#EXT-X-MEDIA-SEQUENCE:102
|
||||
#EXTINF:6,
|
||||
fileSequence102.ts
|
||||
#EXT-X-SKIP:SKIPPED-SEGMENTS=3
|
||||
#EXTINF:6,
|
||||
fileSequence106.ts`;
|
||||
const details = M3U8Parser.parseLevelPlaylist(
|
||||
level,
|
||||
'http://example.com/hls/index.m3u8',
|
||||
0,
|
||||
PlaylistLevelType.MAIN,
|
||||
0,
|
||||
null,
|
||||
);
|
||||
expect(details.playlistParsingError).to.be.null;
|
||||
expect(details.startSN).to.equal(102);
|
||||
expect(details.skippedSegments).to.equal(3);
|
||||
// sn 102, then 3 skipped placeholders (sn 103..105), then sn 106
|
||||
expect(details.fragments.length).to.equal(5);
|
||||
expect(details.fragments[0]?.sn).to.equal(102);
|
||||
expect(details.fragments[1]).to.equal(null);
|
||||
expect(details.fragments[3]).to.equal(null);
|
||||
expect(details.fragments[4]?.sn).to.equal(106);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user