90cc4437 by David LaPalomento

Merge pull request #78 from videojs/feature/xhr-timeouts

Abort XHRs after 45s and trigger an error
2 parents 47c9680d af5557a7
...@@ -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 abortTimeout;
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 abortTimeout = 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(abortTimeout);
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();
......