cd134feb by David LaPalomento

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.
1 parent ce47528c
...@@ -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]) {
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 0, 3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [ 4 "segments": [
6 { 5 {
7 "duration": 10, 6 "duration": 10,
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 1, 3 "mediaSequence": 1,
4 "playlistType": "VOD",
5 "segments": [ 4 "segments": [
6 { 5 {
7 "duration": 6.64, 6 "duration": 6.64,
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 0, 3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [ 4 "segments": [
6 { 5 {
7 "duration": 10, 6 "duration": 10,
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 0, 3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [ 4 "segments": [
6 { 5 {
7 "duration": 10, 6 "duration": 10,
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 0, 3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [ 4 "segments": [
6 { 5 {
7 "duration": 10, 6 "duration": 10,
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 0, 3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [ 4 "segments": [
6 { 5 {
7 "duration": 10, 6 "duration": 10,
......
1 { 1 {
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 0, 3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "targetDuration": 10, 4 "targetDuration": 10,
6 "segments": [{ 5 "segments": [{
7 "uri": "001.ts" 6 "uri": "001.ts"
......