badf3bca by David LaPalomento

Abort XHRs after 45s and trigger an error

Playlist and segment requests will now abort. Segment requests should continue to be retried automatically. Playlist requests will trigger a MEDIA_ERR_NETWORK.
1 parent 47c9680d
...@@ -50,6 +50,7 @@ ...@@ -50,6 +50,7 @@
50 PlaylistLoader = function(srcUrl, withCredentials) { 50 PlaylistLoader = function(srcUrl, withCredentials) {
51 var 51 var
52 loader = this, 52 loader = this,
53 dispose,
53 media, 54 media,
54 mediaUpdateTimeout, 55 mediaUpdateTimeout,
55 request, 56 request,
...@@ -105,11 +106,18 @@ ...@@ -105,11 +106,18 @@
105 106
106 loader.state = 'HAVE_NOTHING'; 107 loader.state = 'HAVE_NOTHING';
107 108
109 // capture the prototype dispose function
110 dispose = this.dispose;
111
112 /**
113 * Abort any outstanding work and clean up.
114 */
108 loader.dispose = function() { 115 loader.dispose = function() {
109 if (request) { 116 if (request) {
110 request.abort(); 117 request.abort();
111 } 118 }
112 window.clearTimeout(mediaUpdateTimeout); 119 window.clearTimeout(mediaUpdateTimeout);
120 dispose.call(this);
113 }; 121 };
114 122
115 /** 123 /**
......
...@@ -50,6 +50,12 @@ ...@@ -50,6 +50,12 @@
50 callbacks[i].apply(this, args); 50 callbacks[i].apply(this, args);
51 } 51 }
52 }; 52 };
53 /**
54 * Destroys the stream and cleans up.
55 */
56 this.dispose = function() {
57 listeners = {};
58 };
53 }; 59 };
54 }; 60 };
55 /** 61 /**
......
...@@ -547,9 +547,11 @@ videojs.Hls.canPlaySource = function(srcObj) { ...@@ -547,9 +547,11 @@ videojs.Hls.canPlaySource = function(srcObj) {
547 xhr = videojs.Hls.xhr = function(url, callback) { 547 xhr = videojs.Hls.xhr = function(url, callback) {
548 var 548 var
549 options = { 549 options = {
550 method: 'GET' 550 method: 'GET',
551 timeout: 45 * 1000
551 }, 552 },
552 request; 553 request,
554 timeout;
553 555
554 if (typeof callback !== 'function') { 556 if (typeof callback !== 'function') {
555 callback = function() {}; 557 callback = function() {};
...@@ -570,6 +572,18 @@ xhr = videojs.Hls.xhr = function(url, callback) { ...@@ -570,6 +572,18 @@ xhr = videojs.Hls.xhr = function(url, callback) {
570 if (options.withCredentials) { 572 if (options.withCredentials) {
571 request.withCredentials = true; 573 request.withCredentials = true;
572 } 574 }
575 if (options.timeout) {
576 if (request.timeout === 0) {
577 request.timeout = options.timeout;
578 } else {
579 // polyfill XHR2 by aborting after the timeout
580 timeout = window.setTimeout(function() {
581 if (request.readystate !== 4) {
582 request.abort();
583 }
584 }, options.timeout);
585 }
586 }
573 587
574 request.onreadystatechange = function() { 588 request.onreadystatechange = function() {
575 // wait until the request completes 589 // wait until the request completes
...@@ -577,6 +591,9 @@ xhr = videojs.Hls.xhr = function(url, callback) { ...@@ -577,6 +591,9 @@ xhr = videojs.Hls.xhr = function(url, callback) {
577 return; 591 return;
578 } 592 }
579 593
594 // clear outstanding timeouts
595 window.clearTimeout(timeout);
596
580 // request error 597 // request error
581 if (this.status >= 400 || this.status === 0) { 598 if (this.status >= 400 || this.status === 0) {
582 return callback.call(this, true, url); 599 return callback.call(this, true, url);
......
...@@ -22,6 +22,8 @@ ...@@ -22,6 +22,8 @@
22 sinonXhr = sinon.useFakeXMLHttpRequest(); 22 sinonXhr = sinon.useFakeXMLHttpRequest();
23 requests = []; 23 requests = [];
24 sinonXhr.onCreate = function(xhr) { 24 sinonXhr.onCreate = function(xhr) {
25 // force the XHR2 timeout polyfill
26 xhr.timeout = undefined;
25 requests.push(xhr); 27 requests.push(xhr);
26 }; 28 };
27 29
...@@ -426,4 +428,17 @@ ...@@ -426,4 +428,17 @@
426 loader.dispose(); 428 loader.dispose();
427 ok(requests[0].aborted, 'refresh request aborted'); 429 ok(requests[0].aborted, 'refresh request aborted');
428 }); 430 });
431
432 test('errors if requests take longer than 45s', function() {
433 var
434 loader = new videojs.Hls.PlaylistLoader('media.m3u8'),
435 errors = 0;
436 loader.on('error', function() {
437 errors++;
438 });
439 clock.tick(45 * 1000);
440
441 strictEqual(errors, 1, 'fired one error');
442 strictEqual(loader.error.code, 2, 'fired a network error');
443 });
429 })(window); 444 })(window);
......
...@@ -78,7 +78,7 @@ var ...@@ -78,7 +78,7 @@ var
78 78
79 request.response = new Uint8Array([1]).buffer; 79 request.response = new Uint8Array([1]).buffer;
80 request.respond(200, 80 request.respond(200,
81 {'Content-Type': contentType}, 81 { 'Content-Type': contentType },
82 window.manifests[manifestName]); 82 window.manifests[manifestName]);
83 }, 83 },
84 84
...@@ -360,13 +360,6 @@ test('downloads media playlists after loading the master', function() { ...@@ -360,13 +360,6 @@ test('downloads media playlists after loading the master', function() {
360 }); 360 });
361 361
362 test('timeupdates do not check to fill the buffer until a media playlist is ready', function() { 362 test('timeupdates do not check to fill the buffer until a media playlist is ready', function() {
363 var urls = [];
364 window.XMLHttpRequest = function() {
365 this.open = function(method, url) {
366 urls.push(url);
367 };
368 this.send = function() {};
369 };
370 player.src({ 363 player.src({
371 src: 'manifest/media.m3u8', 364 src: 'manifest/media.m3u8',
372 type: 'application/vnd.apple.mpegurl' 365 type: 'application/vnd.apple.mpegurl'
...@@ -376,8 +369,8 @@ test('timeupdates do not check to fill the buffer until a media playlist is read ...@@ -376,8 +369,8 @@ test('timeupdates do not check to fill the buffer until a media playlist is read
376 }); 369 });
377 player.trigger('timeupdate'); 370 player.trigger('timeupdate');
378 371
379 strictEqual(1, urls.length, 'one request was made'); 372 strictEqual(1, requests.length, 'one request was made');
380 strictEqual('manifest/media.m3u8', urls[0], 'media playlist requested'); 373 strictEqual('manifest/media.m3u8', requests[0].url, 'media playlist requested');
381 }); 374 });
382 375
383 test('calculates the bandwidth after downloading a segment', function() { 376 test('calculates the bandwidth after downloading a segment', function() {
...@@ -707,7 +700,6 @@ test('stops downloading segments at the end of the playlist', function() { ...@@ -707,7 +700,6 @@ test('stops downloading segments at the end of the playlist', function() {
707 }); 700 });
708 701
709 test('only makes one segment request at a time', function() { 702 test('only makes one segment request at a time', function() {
710 var openedXhrs = 0;
711 player.src({ 703 player.src({
712 src: 'manifest/media.m3u8', 704 src: 'manifest/media.m3u8',
713 type: 'application/vnd.apple.mpegurl' 705 type: 'application/vnd.apple.mpegurl'
...@@ -715,23 +707,12 @@ test('only makes one segment request at a time', function() { ...@@ -715,23 +707,12 @@ test('only makes one segment request at a time', function() {
715 player.hls.mediaSource.trigger({ 707 player.hls.mediaSource.trigger({
716 type: 'sourceopen' 708 type: 'sourceopen'
717 }); 709 });
718 xhr.restore(); 710 standardXHRResponse(requests.pop());
719 var oldXHR = window.XMLHttpRequest;
720 // mock out a long-running XHR
721 window.XMLHttpRequest = function() {
722 this.send = function() {};
723 this.open = function() {
724 openedXhrs++;
725 };
726 };
727 standardXHRResponse(requests[0]);
728 player.trigger('timeupdate'); 711 player.trigger('timeupdate');
729 712
730 strictEqual(1, openedXhrs, 'one XHR is made'); 713 strictEqual(1, requests.length, 'one XHR is made');
731 player.trigger('timeupdate'); 714 player.trigger('timeupdate');
732 strictEqual(1, openedXhrs, 'only one XHR is made'); 715 strictEqual(1, requests.length, 'only one XHR is made');
733 window.XMLHttpRequest = oldXHR;
734 xhr = sinon.useFakeXMLHttpRequest();
735 }); 716 });
736 717
737 test('cancels outstanding XHRs when seeking', function() { 718 test('cancels outstanding XHRs when seeking', function() {
...@@ -791,7 +772,6 @@ test('flushes the parser after each segment', function() { ...@@ -791,7 +772,6 @@ test('flushes the parser after each segment', function() {
791 772
792 test('drops tags before the target timestamp when seeking', function() { 773 test('drops tags before the target timestamp when seeking', function() {
793 var i = 10, 774 var i = 10,
794 callbacks = [],
795 tags = [], 775 tags = [],
796 bytes = []; 776 bytes = [];
797 777
...@@ -803,10 +783,6 @@ test('drops tags before the target timestamp when seeking', function() { ...@@ -803,10 +783,6 @@ test('drops tags before the target timestamp when seeking', function() {
803 }; 783 };
804 this.abort = function() {}; 784 this.abort = function() {};
805 }; 785 };
806 // capture timeouts
807 window.setTimeout = function(callback) {
808 callbacks.push(callback);
809 };
810 786
811 // push a tag into the buffer 787 // push a tag into the buffer
812 tags.push({ pts: 0, bytes: 0 }); 788 tags.push({ pts: 0, bytes: 0 });
...@@ -820,9 +796,6 @@ test('drops tags before the target timestamp when seeking', function() { ...@@ -820,9 +796,6 @@ test('drops tags before the target timestamp when seeking', function() {
820 }); 796 });
821 standardXHRResponse(requests[0]); 797 standardXHRResponse(requests[0]);
822 standardXHRResponse(requests[1]); 798 standardXHRResponse(requests[1]);
823 while (callbacks.length) {
824 callbacks.shift()();
825 }
826 799
827 // mock out a new segment of FLV tags 800 // mock out a new segment of FLV tags
828 bytes = []; 801 bytes = [];
...@@ -835,21 +808,17 @@ test('drops tags before the target timestamp when seeking', function() { ...@@ -835,21 +808,17 @@ test('drops tags before the target timestamp when seeking', function() {
835 player.currentTime(7); 808 player.currentTime(7);
836 standardXHRResponse(requests[2]); 809 standardXHRResponse(requests[2]);
837 810
838 while (callbacks.length) {
839 callbacks.shift()();
840 }
841
842 deepEqual(bytes, [7,8,9], 'three tags are appended'); 811 deepEqual(bytes, [7,8,9], 'three tags are appended');
843 }); 812 });
844 813
845 test('clears pending buffer updates when seeking', function() { 814 test('calls abort() on the SourceBuffer before seeking', function() {
846 var 815 var
847 bytes = [],
848 callbacks = [],
849 aborts = 0, 816 aborts = 0,
817 bytes = [],
850 tags = [{ pts: 0, bytes: 0 }]; 818 tags = [{ pts: 0, bytes: 0 }];
851 819
852 // mock out the parser and source buffer 820
821 // track calls to abort()
853 videojs.Hls.SegmentParser = mockSegmentParser(tags); 822 videojs.Hls.SegmentParser = mockSegmentParser(tags);
854 window.videojs.SourceBuffer = function() { 823 window.videojs.SourceBuffer = function() {
855 this.appendBuffer = function(chunk) { 824 this.appendBuffer = function(chunk) {
...@@ -859,12 +828,7 @@ test('clears pending buffer updates when seeking', function() { ...@@ -859,12 +828,7 @@ test('clears pending buffer updates when seeking', function() {
859 aborts++; 828 aborts++;
860 }; 829 };
861 }; 830 };
862 // capture timeouts
863 window.setTimeout = function(callback) {
864 callbacks.push(callback);
865 };
866 831
867 // queue up a tag to be pushed into the buffer (but don't push it yet!)
868 player.src({ 832 player.src({
869 src: 'manifest/media.m3u8', 833 src: 'manifest/media.m3u8',
870 type: 'application/vnd.apple.mpegurl' 834 type: 'application/vnd.apple.mpegurl'
...@@ -881,10 +845,6 @@ test('clears pending buffer updates when seeking', function() { ...@@ -881,10 +845,6 @@ test('clears pending buffer updates when seeking', function() {
881 player.currentTime(7); 845 player.currentTime(7);
882 standardXHRResponse(requests[2]); 846 standardXHRResponse(requests[2]);
883 847
884 while (callbacks.length) {
885 callbacks.shift()();
886 }
887
888 strictEqual(1, aborts, 'aborted pending buffer'); 848 strictEqual(1, aborts, 'aborted pending buffer');
889 }); 849 });
890 850
...@@ -957,23 +917,6 @@ test('duration is Infinity for live playlists', function() { ...@@ -957,23 +917,6 @@ test('duration is Infinity for live playlists', function() {
957 strictEqual(player.duration(), Infinity, 'duration is infinity'); 917 strictEqual(player.duration(), Infinity, 'duration is infinity');
958 }); 918 });
959 919
960 test('does not reload playlists with an endlist tag', function() {
961 var callbacks = [];
962 // capture timeouts
963 window.setTimeout = function(callback, timeout) {
964 callbacks.push({ callback: callback, timeout: timeout });
965 };
966 player.src({
967 src: 'manifest/media.m3u8',
968 type: 'application/vnd.apple.mpegurl'
969 });
970 player.hls.mediaSource.trigger({
971 type: 'sourceopen'
972 });
973
974 strictEqual(0, callbacks.length, 'no refresh was scheduled');
975 });
976
977 test('updates the media index when a playlist reloads', function() { 920 test('updates the media index when a playlist reloads', function() {
978 player.src({ 921 player.src({
979 src: 'http://example.com/live-updating.m3u8', 922 src: 'http://example.com/live-updating.m3u8',
...@@ -1017,10 +960,6 @@ test('mediaIndex is zero before the first segment loads', function() { ...@@ -1017,10 +960,6 @@ test('mediaIndex is zero before the first segment loads', function() {
1017 '#EXTM3U\n' + 960 '#EXTM3U\n' +
1018 '#EXTINF:10,\n' + 961 '#EXTINF:10,\n' +
1019 '0.ts\n'; 962 '0.ts\n';
1020 window.XMLHttpRequest = function() {
1021 this.open = function() {};
1022 this.send = function() {};
1023 };
1024 player.src({ 963 player.src({
1025 src: 'http://example.com/first-seg-load.m3u8', 964 src: 'http://example.com/first-seg-load.m3u8',
1026 type: 'application/vnd.apple.mpegurl' 965 type: 'application/vnd.apple.mpegurl'
...@@ -1071,24 +1010,6 @@ test('reloads out-of-date live playlists when switching variants', function() { ...@@ -1071,24 +1010,6 @@ test('reloads out-of-date live playlists when switching variants', function() {
1071 strictEqual(player.mediaIndex, 1, 'mediaIndex points at the next segment'); 1010 strictEqual(player.mediaIndex, 1, 'mediaIndex points at the next segment');
1072 }); 1011 });
1073 1012
1074 test('does not reload master playlists', function() {
1075 var callbacks = [];
1076 window.setTimeout = function(callback) {
1077 callbacks.push(callback);
1078 };
1079
1080 player.src({
1081 src: 'http://example.com/master.m3u8',
1082 type: 'application/vnd.apple.mpegurl'
1083 });
1084 player.hls.mediaSource.trigger({
1085 type: 'sourceopen'
1086 });
1087
1088 strictEqual(callbacks.length,
1089 0, 'no reload scheduled');
1090 });
1091
1092 test('if withCredentials option is used, withCredentials is set on the XHR object', function() { 1013 test('if withCredentials option is used, withCredentials is set on the XHR object', function() {
1093 player.dispose(); 1014 player.dispose();
1094 player = createPlayer({ 1015 player = createPlayer({
...@@ -1126,7 +1047,7 @@ test('does not break if the playlist has no segments', function() { ...@@ -1126,7 +1047,7 @@ test('does not break if the playlist has no segments', function() {
1126 }); 1047 });
1127 1048
1128 test('disposes the playlist loader', function() { 1049 test('disposes the playlist loader', function() {
1129 var disposes = 0, player; 1050 var disposes = 0, player, loaderDispose;
1130 player = createPlayer(); 1051 player = createPlayer();
1131 player.src({ 1052 player.src({
1132 src: 'manifest/master.m3u8', 1053 src: 'manifest/master.m3u8',
...@@ -1135,8 +1056,10 @@ test('disposes the playlist loader', function() { ...@@ -1135,8 +1056,10 @@ test('disposes the playlist loader', function() {
1135 player.hls.mediaSource.trigger({ 1056 player.hls.mediaSource.trigger({
1136 type: 'sourceopen' 1057 type: 'sourceopen'
1137 }); 1058 });
1059 loaderDispose = player.hls.playlists.dispose;
1138 player.hls.playlists.dispose = function() { 1060 player.hls.playlists.dispose = function() {
1139 disposes++; 1061 disposes++;
1062 loaderDispose.call(player.hls.playlists);
1140 }; 1063 };
1141 1064
1142 player.dispose(); 1065 player.dispose();
......