50efd48c by David LaPalomento

Basic support for master playlists

If a master playlist has been downloaded, immediately fetch the default variant playlist and start buffering it. This matches HLS network activity in Safari on OS X which also seems to lazily load the non-default variant streams. Consolidate relative URL resolution and use a solution involving the `base` element to take advantage of browser logic for URL composition. Update test cases to expect absolute URLs for XHRs after the initial manifest request.
1 parent 98a64cbe
...@@ -412,7 +412,7 @@ ...@@ -412,7 +412,7 @@
412 this.manifest.totalDuration = calculatedDuration; 412 this.manifest.totalDuration = calculatedDuration;
413 this.trigger('info', { 413 this.trigger('info', {
414 message: 'updating total duration to use a calculated value' 414 message: 'updating total duration to use a calculated value'
415 }) 415 });
416 } 416 }
417 } 417 }
418 })[entry.tagType] || noop).call(self); 418 })[entry.tagType] || noop).call(self);
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
6 * All rights reserved. 6 * All rights reserved.
7 */ 7 */
8 8
9 (function(window, videojs, undefined) { 9 (function(window, videojs, document, undefined) {
10 10
11 videojs.hls = {}; 11 videojs.hls = {};
12 12
...@@ -15,6 +15,45 @@ var ...@@ -15,6 +15,45 @@ var
15 goalBufferLength = 5, 15 goalBufferLength = 5,
16 16
17 /** 17 /**
18 * Constructs a new URI by interpreting a path relative to another
19 * URI.
20 * @param basePath {string} a relative or absolute URI
21 * @param path {string} a path part to combine with the base
22 * @return {string} a URI that is equivalent to composing `base`
23 * with `path`
24 * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
25 */
26 resolveUrl = function(basePath, path) {
27 // use the base element to get the browser to handle URI resolution
28 var
29 oldBase = document.querySelector('base'),
30 docHead = document.querySelector('head'),
31 a = document.createElement('a'),
32 base = oldBase,
33 oldHref,
34 result;
35
36 // prep the document
37 if (oldBase) {
38 oldHref = oldBase.href;
39 } else {
40 base = docHead.appendChild(document.createElement('base'));
41 }
42
43 base.href = basePath;
44 a.href = path;
45 result = a.href;
46
47 // clean up
48 if (oldBase) {
49 oldBase.href = oldHref;
50 } else {
51 docHead.removeChild(base);
52 }
53 return result;
54 },
55
56 /**
18 * Initializes the HLS plugin. 57 * Initializes the HLS plugin.
19 * @param options {mixed} the URL to an HLS playlist 58 * @param options {mixed} the URL to an HLS playlist
20 */ 59 */
...@@ -24,21 +63,21 @@ var ...@@ -24,21 +63,21 @@ var
24 segmentParser = new videojs.hls.SegmentParser(), 63 segmentParser = new videojs.hls.SegmentParser(),
25 player = this, 64 player = this,
26 extname, 65 extname,
27 url, 66 srcUrl,
28 67
29 segmentXhr, 68 segmentXhr,
30 fillBuffer,
31 onDurationUpdate, 69 onDurationUpdate,
32 selectPlaylist; 70 downloadPlaylist,
71 fillBuffer;
33 72
34 extname = (/[^#?]*(?:\/[^#?]*\.([^#?]*))/).exec(player.currentSrc()); 73 extname = (/[^#?]*(?:\/[^#?]*\.([^#?]*))/).exec(player.currentSrc());
35 if (typeof options === 'string') { 74 if (typeof options === 'string') {
36 url = options; 75 srcUrl = options;
37 } else if (options) { 76 } else if (options) {
38 url = options.url; 77 srcUrl = options.url;
39 } else if (extname && extname[1] === 'm3u8') { 78 } else if (extname && extname[1] === 'm3u8') {
40 // if the currentSrc looks like an m3u8, attempt to use it 79 // if the currentSrc looks like an m3u8, attempt to use it
41 url = player.currentSrc(); 80 srcUrl = player.currentSrc();
42 } else { 81 } else {
43 // do nothing until the plugin is initialized with a valid URL 82 // do nothing until the plugin is initialized with a valid URL
44 videojs.log('hls: no valid playlist URL specified'); 83 videojs.log('hls: no valid playlist URL specified');
...@@ -47,24 +86,20 @@ var ...@@ -47,24 +86,20 @@ var
47 86
48 // expose the HLS plugin state 87 // expose the HLS plugin state
49 player.hls.readyState = function() { 88 player.hls.readyState = function() {
50 if (!player.hls.manifest) { 89 if (!player.hls.media) {
51 return 0; // HAVE_NOTHING 90 return 0; // HAVE_NOTHING
52 } 91 }
53 return 1; // HAVE_METADATA 92 return 1; // HAVE_METADATA
54 }; 93 };
55 94
56 // load the MediaSource into the player
57 mediaSource.addEventListener('sourceopen', function() {
58 // construct the video data buffer and set the appropriate MIME type
59 var sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"');
60 player.hls.sourceBuffer = sourceBuffer;
61 sourceBuffer.appendBuffer(segmentParser.getFlvHeader());
62 95
63 // Chooses the appropriate media playlist based on the current bandwidth 96 /**
64 // estimate and the player size 97 * Chooses the appropriate media playlist based on the current
65 selectPlaylist = function() { 98 * bandwidth estimate and the player size.
66 player.hls.currentPlaylist = player.hls.manifest; 99 */
67 player.hls.currentMediaIndex = 0; 100 player.hls.selectPlaylist = function() {
101 player.hls.media = player.hls.master.playlists[0];
102 player.hls.mediaIndex = 0;
68 }; 103 };
69 104
70 onDurationUpdate = function(value) { 105 onDurationUpdate = function(value) {
...@@ -72,6 +107,64 @@ var ...@@ -72,6 +107,64 @@ var
72 }; 107 };
73 108
74 /** 109 /**
110 * Download an M3U8 and update the current manifest object. If the provided
111 * URL is a master playlist, the default variant will be downloaded and
112 * parsed as well. Triggers `loadedmanifest` once for each playlist that is
113 * downloaded and `loadedmetadata` after at least one media playlist has
114 * been parsed. Whether multiple playlists were downloaded or not, after
115 * `loadedmetadata` fires a parsed or inferred master playlist object will
116 * be available as `player.hls.master`.
117 *
118 * @param url {string} a URL to the M3U8 file to process
119 */
120 downloadPlaylist = function(url) {
121 var xhr = new window.XMLHttpRequest();
122 xhr.open('GET', url);
123 xhr.onreadystatechange = function() {
124 var i, parser, playlist, playlistUri;
125
126 if (xhr.readyState === 4) {
127 // readystate DONE
128 parser = new videojs.m3u8.Parser();
129 parser.on('durationUpdate', onDurationUpdate);
130 parser.push(xhr.responseText);
131
132 // master playlists
133 if (parser.manifest.playlists) {
134 player.hls.master = parser.manifest;
135 downloadPlaylist(resolveUrl(url, parser.manifest.playlists[0].uri));
136 player.trigger('loadedmanifest');
137 return;
138 }
139
140 // media playlists
141 if (player.hls.master) {
142 // merge this playlist into the master
143 i = player.hls.master.playlists.length;
144 while (i--) {
145 playlist = player.hls.master.playlists[i];
146 playlistUri = resolveUrl(srcUrl, playlist.uri);
147 if (playlistUri === url) {
148 player.hls.master.playlists[i] =
149 videojs.util.mergeOptions(playlist, parser.manifest);
150 }
151 }
152 } else {
153 // infer a master playlist if none was previously requested
154 player.hls.master = {
155 playlists: [parser.manifest]
156 };
157 }
158
159 player.hls.selectPlaylist();
160 player.trigger('loadedmanifest');
161 player.trigger('loadedmetadata');
162 }
163 };
164 xhr.send(null);
165 };
166
167 /**
75 * Determines whether there is enough video data currently in the buffer 168 * Determines whether there is enough video data currently in the buffer
76 * and downloads a new segment if the buffered time is less than the goal. 169 * and downloads a new segment if the buffered time is less than the goal.
77 */ 170 */
...@@ -79,7 +172,7 @@ var ...@@ -79,7 +172,7 @@ var
79 var 172 var
80 buffered = player.buffered(), 173 buffered = player.buffered(),
81 bufferedTime = 0, 174 bufferedTime = 0,
82 segment = player.hls.currentPlaylist.segments[player.hls.currentMediaIndex], 175 segment = player.hls.media.segments[player.hls.mediaIndex],
83 segmentUri, 176 segmentUri,
84 startTime; 177 startTime;
85 178
...@@ -103,25 +196,8 @@ var ...@@ -103,25 +196,8 @@ var
103 return; 196 return;
104 } 197 }
105 198
106 segmentUri = segment.uri; 199 segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.media.uri || ''),
107 if ((/^\/[^\/]/).test(segmentUri)) { 200 segment.uri);
108 // the segment is specified with a network path,
109 // e.g. "/01.ts"
110 (function() {
111 // use an anchor to resolve the manifest URL to an absolute path
112 // this method should work back to IE6:
113 // http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
114 var resolver = document.createElement('div');
115 resolver.innerHTML = '<a href="' + url + '"></a>';
116
117 segmentUri = (/^[A-z]*:\/\/[^\/]*/).exec(resolver.firstChild.href)[0] +
118 segmentUri;
119 })();
120 } else if (!(/^([A-z]*:)?\/\//).test(segmentUri)) {
121 // the segment is specified with a relative path,
122 // e.g. "../01.ts" or "path/to/01.ts"
123 segmentUri = url.split('/').slice(0, -1).concat(segmentUri).join('/');
124 }
125 201
126 // request the next segment 202 // request the next segment
127 segmentXhr = new window.XMLHttpRequest(); 203 segmentXhr = new window.XMLHttpRequest();
...@@ -141,43 +217,24 @@ var ...@@ -141,43 +217,24 @@ var
141 } 217 }
142 218
143 segmentXhr = null; 219 segmentXhr = null;
144 player.hls.currentMediaIndex++; 220 player.hls.mediaIndex++;
145 } 221 }
146 }; 222 };
147 startTime = +new Date(); 223 startTime = +new Date();
148 segmentXhr.send(null); 224 segmentXhr.send(null);
149 }; 225 };
150 player.on('loadedmetadata', fillBuffer);
151 player.on('timeupdate', fillBuffer);
152
153 // download and process the manifest
154 (function() {
155 var xhr = new window.XMLHttpRequest();
156 xhr.open('GET', url);
157 xhr.onreadystatechange = function() {
158 var parser;
159
160 if (xhr.readyState === 4) {
161 // readystate DONE
162 parser = new videojs.m3u8.Parser();
163 parser.on('durationUpdate', onDurationUpdate);
164 parser.push(xhr.responseText);
165 player.hls.manifest = parser.manifest;
166 226
167 if(parser.manifest.totalDuration) { 227 // load the MediaSource into the player
168 player.duration(parser.manifest.totalDuration); 228 mediaSource.addEventListener('sourceopen', function() {
169 } 229 // construct the video data buffer and set the appropriate MIME type
230 var sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"');
231 player.hls.sourceBuffer = sourceBuffer;
232 sourceBuffer.appendBuffer(segmentParser.getFlvHeader());
170 233
171 player.trigger('loadedmanifest'); 234 player.on('loadedmetadata', fillBuffer);
235 player.on('timeupdate', fillBuffer);
172 236
173 if (parser.manifest.segments) { 237 downloadPlaylist(srcUrl);
174 selectPlaylist();
175 player.trigger('loadedmetadata');
176 }
177 }
178 };
179 xhr.send(null);
180 })();
181 }); 238 });
182 player.src({ 239 player.src({
183 src: videojs.URL.createObjectURL(mediaSource), 240 src: videojs.URL.createObjectURL(mediaSource),
...@@ -195,4 +252,4 @@ videojs.plugin('hls', function() { ...@@ -195,4 +252,4 @@ videojs.plugin('hls', function() {
195 initialize().apply(this, arguments); 252 initialize().apply(this, arguments);
196 }); 253 });
197 254
198 })(window, window.videojs); 255 })(window, window.videojs, document);
......
1 #EXTM3U 1 #EXTM3U
2 #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=200000 2 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000
3 prog_index.m3u8 3 prog_index.m3u8
4 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000
5 prog_index1.m3u8
6 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000
7 prog_index2.m3u8
8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000
9 prog_index3.m3u8
......
1 {
2 "allowCache": true,
3 "playlists": [{
4 "attributes": {
5 "PROGRAM-ID": 1,
6 "BANDWIDTH": 240000,
7 "RESOLUTION": {
8 "width": 396,
9 "height": 224
10 }
11 },
12 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
13 }, {
14 "attributes": {
15 "PROGRAM-ID": 1,
16 "BANDWIDTH": 40000
17 },
18 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
19 }, {
20 "attributes": {
21 "PROGRAM-ID": 1,
22 "BANDWIDTH": 440000,
23 "RESOLUTION": {
24 "width": 396,
25 "height": 224
26 }
27 },
28 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
29 }, {
30 "attributes": {
31 "PROGRAM-ID": 1,
32 "BANDWIDTH": 1928000,
33 "RESOLUTION": {
34 "width": 960,
35 "height": 540
36 }
37 },
38 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
39 }]
40 }
1 #EXTM3U
2 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=396x224
3 http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001
4 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000
5 http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001
6 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=396x224
7 http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001
8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000,RESOLUTION=960x540
9 http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001
...@@ -10,13 +10,13 @@ ...@@ -10,13 +10,13 @@
10 "height": 224 10 "height": 224
11 } 11 }
12 }, 12 },
13 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001" 13 "uri": "media.m3u8"
14 }, { 14 }, {
15 "attributes": { 15 "attributes": {
16 "PROGRAM-ID": 1, 16 "PROGRAM-ID": 1,
17 "BANDWIDTH": 40000 17 "BANDWIDTH": 40000
18 }, 18 },
19 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001" 19 "uri": "media.m3u8"
20 }, { 20 }, {
21 "attributes": { 21 "attributes": {
22 "PROGRAM-ID": 1, 22 "PROGRAM-ID": 1,
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
26 "height": 224 26 "height": 224
27 } 27 }
28 }, 28 },
29 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001" 29 "uri": "media.m3u8"
30 }, { 30 }, {
31 "attributes": { 31 "attributes": {
32 "PROGRAM-ID": 1, 32 "PROGRAM-ID": 1,
...@@ -36,6 +36,6 @@ ...@@ -36,6 +36,6 @@
36 "height": 540 36 "height": 540
37 } 37 }
38 }, 38 },
39 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" 39 "uri": "media.m3u8"
40 }] 40 }]
41 } 41 }
......
1 # A simple master playlist with multiple variant streams
1 #EXTM3U 2 #EXTM3U
2 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=396x224 3 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=396x224
3 http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001 4 media.m3u8
4 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000 5 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000
5 http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001 6 media.m3u8
6 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=396x224 7 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=396x224
7 http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001 8 media.m3u8
8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000,RESOLUTION=960x540 9 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000,RESOLUTION=960x540
9 http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001 10 media.m3u8
......
...@@ -63,7 +63,7 @@ module('HLS', { ...@@ -63,7 +63,7 @@ module('HLS', {
63 this.send = function() { 63 this.send = function() {
64 // if the request URL looks like one of the test manifests, grab the 64 // if the request URL looks like one of the test manifests, grab the
65 // contents off the global object 65 // contents off the global object
66 var manifestName = (/.*\/(.*)\.m3u8/).exec(xhrUrls.slice(-1)[0]); 66 var manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(xhrUrls.slice(-1)[0]);
67 if (manifestName) { 67 if (manifestName) {
68 manifestName = manifestName[1]; 68 manifestName = manifestName[1];
69 } 69 }
...@@ -99,10 +99,11 @@ test('loads the specified manifest URL on init', function() { ...@@ -99,10 +99,11 @@ test('loads the specified manifest URL on init', function() {
99 }); 99 });
100 ok(loadedmanifest, 'loadedmanifest fires'); 100 ok(loadedmanifest, 'loadedmanifest fires');
101 ok(loadedmetadata, 'loadedmetadata fires'); 101 ok(loadedmetadata, 'loadedmetadata fires');
102 ok(player.hls.manifest, 'the manifest is available'); 102 ok(player.hls.master, 'a master is inferred');
103 ok(player.hls.manifest.segments, 'the segment entries are parsed'); 103 ok(player.hls.media, 'the manifest is available');
104 strictEqual(player.hls.manifest, 104 ok(player.hls.media.segments, 'the segment entries are parsed');
105 player.hls.currentPlaylist, 105 strictEqual(player.hls.master.playlists[0],
106 player.hls.media,
106 'the playlist is selected'); 107 'the playlist is selected');
107 strictEqual(player.hls.readyState(), 1, 'the readyState is HAVE_METADATA'); 108 strictEqual(player.hls.readyState(), 1, 'the readyState is HAVE_METADATA');
108 }); 109 });
...@@ -116,7 +117,11 @@ test('starts downloading a segment on loadedmetadata', function() { ...@@ -116,7 +117,11 @@ test('starts downloading a segment on loadedmetadata', function() {
116 type: 'sourceopen' 117 type: 'sourceopen'
117 }); 118 });
118 119
119 strictEqual(xhrUrls[1], 'manifest/00001.ts', 'the first segment is requested'); 120 strictEqual(xhrUrls[1],
121 window.location.origin +
122 window.location.pathname.split('/').slice(0, -1).join('/') +
123 '/manifest/00001.ts',
124 'the first segment is requested');
120 }); 125 });
121 126
122 test('recognizes absolute URIs and requests them unmodified', function() { 127 test('recognizes absolute URIs and requests them unmodified', function() {
...@@ -151,6 +156,26 @@ test('re-initializes the plugin for each source', function() { ...@@ -151,6 +156,26 @@ test('re-initializes the plugin for each source', function() {
151 notStrictEqual(firstInit, secondInit, 'the plugin object is replaced'); 156 notStrictEqual(firstInit, secondInit, 'the plugin object is replaced');
152 }); 157 });
153 158
159 test('downloads media playlists after loading the master', function() {
160 player.hls('manifest/master.m3u8');
161 videojs.mediaSources[player.currentSrc()].trigger({
162 type: 'sourceopen'
163 });
164
165 strictEqual(xhrUrls.length, 3, 'three requests were made');
166 strictEqual(xhrUrls[0], 'manifest/master.m3u8', 'master playlist requested');
167 strictEqual(xhrUrls[1],
168 window.location.origin +
169 window.location.pathname.split('/').slice(0, -1).join('/') +
170 '/manifest/media.m3u8',
171 'media playlist requested');
172 strictEqual(xhrUrls[2],
173 window.location.origin +
174 window.location.pathname.split('/').slice(0, -1).join('/') +
175 '/manifest/00001.ts',
176 'first segment requested');
177 });
178
154 test('calculates the bandwidth after downloading a segment', function() { 179 test('calculates the bandwidth after downloading a segment', function() {
155 player.hls('manifest/media.m3u8'); 180 player.hls('manifest/media.m3u8');
156 videojs.mediaSources[player.currentSrc()].trigger({ 181 videojs.mediaSources[player.currentSrc()].trigger({
...@@ -195,7 +220,11 @@ test('downloads the next segment if the buffer is getting low', function() { ...@@ -195,7 +220,11 @@ test('downloads the next segment if the buffer is getting low', function() {
195 player.trigger('timeupdate'); 220 player.trigger('timeupdate');
196 221
197 strictEqual(xhrUrls.length, 3, 'made a request'); 222 strictEqual(xhrUrls.length, 3, 'made a request');
198 strictEqual(xhrUrls[2], 'manifest/00002.ts', 'made segment request'); 223 strictEqual(xhrUrls[2],
224 window.location.origin +
225 window.location.pathname.split('/').slice(0, -1).join('/') +
226 '/manifest/00002.ts',
227 'made segment request');
199 }); 228 });
200 229
201 test('stops downloading segments at the end of the playlist', function() { 230 test('stops downloading segments at the end of the playlist', function() {
...@@ -204,7 +233,7 @@ test('stops downloading segments at the end of the playlist', function() { ...@@ -204,7 +233,7 @@ test('stops downloading segments at the end of the playlist', function() {
204 type: 'sourceopen' 233 type: 'sourceopen'
205 }); 234 });
206 xhrUrls = []; 235 xhrUrls = [];
207 player.hls.currentMediaIndex = 4; 236 player.hls.mediaIndex = 4;
208 player.trigger('timeupdate'); 237 player.trigger('timeupdate');
209 238
210 strictEqual(xhrUrls.length, 0, 'no request is made'); 239 strictEqual(xhrUrls.length, 0, 'no request is made');
......