Add function to merge an update to a media playlist
When an ENDLIST tag is not present, media playlists should be continually re-requested to check for updates. The updated versions of the playlist must be reconciled with the client's metadata to continue playback. Added a single function to manage this process for media playlists with and without media sequence information, using URIs and byterange information to match segments. Removed parser code that defaulted the playlist type to VOD if no type was specified. The spec allows live streams to omit the playlist type if the server intends to remove segments from the playlist. None of the runtime code actually referenced playlist type so it may have been a bit premature to parse it at all.
Showing
9 changed files
with
174 additions
and
23 deletions
... | @@ -35,7 +35,8 @@ | ... | @@ -35,7 +35,8 @@ |
35 | Stream = videojs.hls.Stream, | 35 | Stream = videojs.hls.Stream, |
36 | LineStream, | 36 | LineStream, |
37 | ParseStream, | 37 | ParseStream, |
38 | Parser; | 38 | Parser, |
39 | merge; | ||
39 | 40 | ||
40 | /** | 41 | /** |
41 | * A stream that buffers string input and generates a `data` event for each | 42 | * A stream that buffers string input and generates a `data` event for each |
... | @@ -345,12 +346,6 @@ | ... | @@ -345,12 +346,6 @@ |
345 | } | 346 | } |
346 | }, | 347 | }, |
347 | 'inf': function() { | 348 | 'inf': function() { |
348 | if (!this.manifest.playlistType) { | ||
349 | this.manifest.playlistType = 'VOD'; | ||
350 | this.trigger('info', { | ||
351 | message: 'defaulting playlist type to VOD' | ||
352 | }); | ||
353 | } | ||
354 | if (!('mediaSequence' in this.manifest)) { | 349 | if (!('mediaSequence' in this.manifest)) { |
355 | this.manifest.mediaSequence = 0; | 350 | this.manifest.mediaSequence = 0; |
356 | this.trigger('info', { | 351 | this.trigger('info', { |
... | @@ -458,9 +453,50 @@ | ... | @@ -458,9 +453,50 @@ |
458 | this.lineStream.push('\n'); | 453 | this.lineStream.push('\n'); |
459 | }; | 454 | }; |
460 | 455 | ||
456 | /** | ||
457 | * Merges two versions of a media playlist. | ||
458 | * @param base {object} the earlier version of the media playlist. | ||
459 | * @param update {object} the updates to apply to the base playlist. | ||
460 | * @return {object} a new media playlist object that combines the | ||
461 | * information in the two arguments. | ||
462 | */ | ||
463 | merge = function(base, update) { | ||
464 | var | ||
465 | result = mergeOptions({}, base, update), | ||
466 | uri = update.segments[0].uri, | ||
467 | i = base.segments.length, | ||
468 | byterange, | ||
469 | segment; | ||
470 | |||
471 | // align and apply the updated segments | ||
472 | while (i--) { | ||
473 | segment = base.segments[i]; | ||
474 | if (uri === segment.uri) { | ||
475 | // if there is no byterange information, match by URI | ||
476 | if (!segment.byterange) { | ||
477 | result.segments = base.segments.slice(0, i).concat(update.segments); | ||
478 | break; | ||
479 | } | ||
480 | // if a byterange is specified, make sure the segments match exactly | ||
481 | byterange = update.segments[0].byterange || {}; | ||
482 | if (segment.byterange.offset === byterange.offset && | ||
483 | segment.byterange.length === byterange.length) { | ||
484 | result.segments = base.segments.slice(0, i).concat(update.segments); | ||
485 | break; | ||
486 | } | ||
487 | } | ||
488 | } | ||
489 | // concatenate the two arrays if there was no overlap | ||
490 | if (i < 0) { | ||
491 | result.segments = base.segments.concat(update.segments); | ||
492 | } | ||
493 | return result; | ||
494 | }; | ||
495 | |||
461 | window.videojs.m3u8 = { | 496 | window.videojs.m3u8 = { |
462 | LineStream: LineStream, | 497 | LineStream: LineStream, |
463 | ParseStream: ParseStream, | 498 | ParseStream: ParseStream, |
464 | Parser: Parser | 499 | Parser: Parser, |
500 | merge: merge | ||
465 | }; | 501 | }; |
466 | })(window.videojs, window.parseInt, window.isFinite, window.videojs.util.mergeOptions); | 502 | })(window.videojs, window.parseInt, window.isFinite, window.videojs.util.mergeOptions); | ... | ... |
1 | (function(window, undefined) { | 1 | (function(window, undefined) { |
2 | var | 2 | var |
3 | //manifestController = this.manifestController, | 3 | //manifestController = this.manifestController, |
4 | ParseStream = window.videojs.m3u8.ParseStream, | 4 | m3u8 = window.videojs.m3u8, |
5 | ParseStream = m3u8.ParseStream, | ||
5 | parseStream, | 6 | parseStream, |
6 | LineStream = window.videojs.m3u8.LineStream, | 7 | LineStream = m3u8.LineStream, |
7 | lineStream, | 8 | lineStream, |
8 | Parser = window.videojs.m3u8.Parser, | 9 | Parser = m3u8.Parser, |
9 | parser; | 10 | parser; |
10 | 11 | ||
11 | /* | 12 | /* |
... | @@ -506,19 +507,140 @@ | ... | @@ -506,19 +507,140 @@ |
506 | ok(!event, 'no event is triggered'); | 507 | ok(!event, 'no event is triggered'); |
507 | }); | 508 | }); |
508 | 509 | ||
509 | module('m3u8 parser', { | 510 | module('m3u8 parser'); |
510 | setup: function() { | 511 | |
512 | test('can be constructed', function() { | ||
513 | notStrictEqual(new Parser(), undefined, 'parser is defined'); | ||
514 | }); | ||
515 | |||
516 | test('merges a manifest that strictly adds to an earlier one', function() { | ||
517 | var key, base, mid, parsed; | ||
518 | for (key in window.manifests) { | ||
519 | if (window.expected[key]) { | ||
520 | manifest = window.manifests[key]; | ||
521 | // parse the first half of the manifest | ||
522 | mid = manifest.length / 2; | ||
523 | parser = new Parser(); | ||
524 | parser.push(manifest.substring(0, mid)); | ||
525 | base = parser.manifest; | ||
526 | if (!base.segments) { | ||
527 | // only test merges for media playlists | ||
528 | continue; | ||
529 | } | ||
530 | |||
531 | // attach the partial manifest to a new parser | ||
511 | parser = new Parser(); | 532 | parser = new Parser(); |
533 | parser.push(manifest); | ||
534 | |||
535 | // merge the manifests together | ||
536 | deepEqual(m3u8.merge(base, parser.manifest), | ||
537 | window.expected[key], | ||
538 | key + '.m3u8 was parsed correctly'); | ||
539 | } | ||
512 | } | 540 | } |
513 | }); | 541 | }); |
514 | 542 | ||
515 | test('should create a parser', function() { | 543 | test('merges overlapping segments without media sequences', function() { |
516 | notStrictEqual(parser, undefined, 'parser is defined'); | 544 | var base; |
545 | parser = new Parser(); | ||
546 | parser.push('#EXTM3U\n'); | ||
547 | parser.push('#EXTINF:10,\n'); | ||
548 | parser.push('0.ts\n'); | ||
549 | parser.push('#EXTINF:10,\n'); | ||
550 | parser.push('1.ts\n'); | ||
551 | base = parser.manifest; | ||
552 | |||
553 | parser = new Parser(); | ||
554 | parser.push('#EXTM3U\n'); | ||
555 | parser.push('#EXTINF:10,\n'); | ||
556 | parser.push('1.ts\n'); | ||
557 | parser.push('#EXTINF:10,\n'); | ||
558 | parser.push('2.ts\n'); | ||
559 | |||
560 | deepEqual({ | ||
561 | allowCache: true, | ||
562 | mediaSequence: 0, | ||
563 | segments: [{ duration: 10, uri: '0.ts'}, | ||
564 | { duration: 10, uri: '1.ts' }, | ||
565 | { duration: 10, uri: '2.ts' }] | ||
566 | }, m3u8.merge(base, parser.manifest), 'merges segment additions'); | ||
567 | }); | ||
568 | |||
569 | test('appends non-overlapping segments without media sequences', function() { | ||
570 | var base; | ||
571 | parser = new Parser(); | ||
572 | parser.push('#EXTM3U\n'); | ||
573 | parser.push('#EXTINF:10,\n'); | ||
574 | parser.push('0.ts\n'); | ||
575 | base = parser.manifest; | ||
576 | |||
577 | parser = new Parser(); | ||
578 | parser.push('#EXTM3U\n'); | ||
579 | parser.push('#EXTINF:10,\n'); | ||
580 | parser.push('1.ts\n'); | ||
581 | |||
582 | deepEqual({ | ||
583 | allowCache: true, | ||
584 | mediaSequence: 0, | ||
585 | segments: [{ duration: 10, uri: '0.ts'}, | ||
586 | { duration: 10, uri: '1.ts' }] | ||
587 | }, m3u8.merge(base, parser.manifest), 'appends segment additions'); | ||
588 | }); | ||
589 | |||
590 | test('replaces segments when merging with a higher media sequence number', function() { | ||
591 | var base; | ||
592 | parser = new Parser(); | ||
593 | parser.push('#EXTM3U\n'); | ||
594 | parser.push('#EXT-X-MEDIA-SEQUENCE:3\n'); | ||
595 | parser.push('#EXTINF:10,\n'); | ||
596 | parser.push('3.ts\n'); | ||
597 | base = parser.manifest; | ||
598 | |||
599 | parser = new Parser(); | ||
600 | parser.push('#EXTM3U\n'); | ||
601 | parser.push('#EXT-X-MEDIA-SEQUENCE:7\n'); | ||
602 | parser.push('#EXTINF:10,\n'); | ||
603 | parser.push('7.ts\n'); | ||
604 | base = parser.manifest; | ||
605 | |||
606 | deepEqual({ | ||
607 | allowCache: true, | ||
608 | mediaSequence: 7, | ||
609 | segments: [{ duration: 10, uri: '7.ts' }] | ||
610 | }, m3u8.merge(base, parser.manifest), 'replaces segments'); | ||
611 | }); | ||
612 | |||
613 | test('replaces overlapping segments when media sequence is present', function() { | ||
614 | var base; | ||
615 | parser = new Parser(); | ||
616 | parser.push('#EXTM3U\n'); | ||
617 | parser.push('#EXT-X-MEDIA-SEQUENCE:3\n'); | ||
618 | parser.push('#EXTINF:10,\n'); | ||
619 | parser.push('3.ts\n'); | ||
620 | parser.push('#EXTINF:10,\n'); | ||
621 | parser.push('4.ts\n'); | ||
622 | base = parser.manifest; | ||
623 | |||
624 | parser = new Parser(); | ||
625 | parser.push('#EXTM3U\n'); | ||
626 | parser.push('#EXT-X-MEDIA-SEQUENCE:4\n'); | ||
627 | parser.push('#EXTINF:10,\n'); | ||
628 | parser.push('4.ts\n'); | ||
629 | parser.push('#EXTINF:10,\n'); | ||
630 | parser.push('5.ts\n'); | ||
631 | base = parser.manifest; | ||
632 | |||
633 | deepEqual({ | ||
634 | allowCache: true, | ||
635 | mediaSequence: 4, | ||
636 | segments: [{ duration: 10, uri: '4.ts' }, | ||
637 | { duration: 10, uri: '5.ts' }] | ||
638 | }, m3u8.merge(base, parser.manifest), 'replaces segments'); | ||
517 | }); | 639 | }); |
518 | 640 | ||
519 | module('m3u8s'); | 641 | module('m3u8s'); |
520 | 642 | ||
521 | test('parses the example manifests as expected', function() { | 643 | test('parses static manifests as expected', function() { |
522 | var key; | 644 | var key; |
523 | for (key in window.manifests) { | 645 | for (key in window.manifests) { |
524 | if (window.expected[key]) { | 646 | if (window.expected[key]) { | ... | ... |
-
Please register or sign in to post a comment