Merge pull request #78 from videojs/feature/xhr-timeouts
Abort XHRs after 45s and trigger an error
Showing
5 changed files
with
61 additions
and
92 deletions
... | @@ -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 | /** | ... | ... |
... | @@ -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(); | ... | ... |
-
Please register or sign in to post a comment