9e8618df by David LaPalomento

Merge pull request #385 from dmlap/custom-codecs

Pass codecs along into source buffer creation
2 parents e8371f66 72ece371
1 language: node_js 1 language: node_js
2 sudo: false
2 node_js: 3 node_js:
3 - '0.10' 4 - "stable"
4 install: 5 install:
5 - npm install -g grunt-cli && npm install 6 - npm install -g grunt-cli && npm install
6 notifications: 7 notifications:
...@@ -11,6 +12,9 @@ notifications: ...@@ -11,6 +12,9 @@ notifications:
11 channels: 12 channels:
12 - "chat.freenode.net#videojs" 13 - "chat.freenode.net#videojs"
13 use_notice: true 14 use_notice: true
15 before_script:
16 - export DISPLAY=:99.0
17 - sh -e /etc/init.d/xvfb start
14 env: 18 env:
15 global: 19 global:
16 - secure: dM7svnHPPu5IiUMeFWW5zg+iuWNpwt6SSDi3MmVvhSclNMRLesQoRB+7Qq5J/LiKhmjpv1/GlNVV0CTsHMRhZNwQ3fo38eEuTXv99aAflEITXwSEh/VntKViHbGFubn06EnVkJoH6MX3zJ6kbiwc2QdSQbywKzS6l6quUEpWpd0= 20 - secure: dM7svnHPPu5IiUMeFWW5zg+iuWNpwt6SSDi3MmVvhSclNMRLesQoRB+7Qq5J/LiKhmjpv1/GlNVV0CTsHMRhZNwQ3fo38eEuTXv99aAflEITXwSEh/VntKViHbGFubn06EnVkJoH6MX3zJ6kbiwc2QdSQbywKzS6l6quUEpWpd0=
......
...@@ -357,7 +357,7 @@ module.exports = function(grunt) { ...@@ -357,7 +357,7 @@ module.exports = function(grunt) {
357 grunt.task.run(['karma:saucelabs']); 357 grunt.task.run(['karma:saucelabs']);
358 grunt.task.run(['connect:test', 'protractor:saucelabs']); 358 grunt.task.run(['connect:test', 'protractor:saucelabs']);
359 } else { 359 } else {
360 grunt.task.run(['karma:phantomjs']); 360 grunt.task.run(['karma:firefox']);
361 } 361 }
362 } else { 362 } else {
363 if (tasks.length === 0) { 363 if (tasks.length === 0) {
......
...@@ -11,8 +11,6 @@ ...@@ -11,8 +11,6 @@
11 11
12 <!-- transmuxing --> 12 <!-- transmuxing -->
13 <script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/lib/stream.js"></script> 13 <script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/lib/stream.js"></script>
14 <script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/lib/mp4-generator.js"></script>
15 <script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/lib/transmuxer.js"></script>
16 <script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/flv-tag.js"></script> 14 <script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/flv-tag.js"></script>
17 <script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/exp-golomb.js"></script> 15 <script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/exp-golomb.js"></script>
18 <script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/h264-extradata.js"></script> 16 <script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/h264-extradata.js"></script>
...@@ -23,6 +21,9 @@ ...@@ -23,6 +21,9 @@
23 21
24 <!-- Media Sources plugin --> 22 <!-- Media Sources plugin -->
25 <script src="node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script> 23 <script src="node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
24 <script>
25 videojs.MediaSource.webWorkerURI = 'node_modules/videojs-contrib-media-sources/src/transmuxer_worker.js';
26 </script>
26 27
27 <!-- HLS plugin --> 28 <!-- HLS plugin -->
28 <script src="src/videojs-hls.js"></script> 29 <script src="src/videojs-hls.js"></script>
...@@ -39,12 +40,6 @@ ...@@ -39,12 +40,6 @@
39 40
40 <script src="src/bin-utils.js"></script> 41 <script src="src/bin-utils.js"></script>
41 42
42 <!-- example MPEG2-TS segments -->
43 <!-- bipbop -->
44 <!-- <script src="test/tsSegment.js"></script> -->
45 <!-- bunnies -->
46 <!--<script src="test/tsSegment-bc.js"></script>-->
47
48 <style> 43 <style>
49 body { 44 body {
50 font-family: Arial, sans-serif; 45 font-family: Arial, sans-serif;
......
...@@ -10,10 +10,10 @@ var ...@@ -10,10 +10,10 @@ var
10 // a fudge factor to apply to advertised playlist bitrates to account for 10 // a fudge factor to apply to advertised playlist bitrates to account for
11 // temporary flucations in client bandwidth 11 // temporary flucations in client bandwidth
12 bandwidthVariance = 1.1, 12 bandwidthVariance = 1.1,
13 Component = videojs.getComponent('Component'),
13 14
14 // the amount of time to wait between checking the state of the buffer 15 // the amount of time to wait between checking the state of the buffer
15 bufferCheckInterval = 500, 16 bufferCheckInterval = 500,
16 Component = videojs.getComponent('Component'),
17 17
18 keyXhr, 18 keyXhr,
19 keyFailed, 19 keyFailed,
...@@ -133,7 +133,7 @@ videojs.Hls.prototype.src = function(src) { ...@@ -133,7 +133,7 @@ videojs.Hls.prototype.src = function(src) {
133 133
134 // We need to trigger this asynchronously to give others the chance 134 // We need to trigger this asynchronously to give others the chance
135 // to bind to the event when a source is set at player creation 135 // to bind to the event when a source is set at player creation
136 setTimeout(function() { 136 this.setTimeout(function() {
137 this.tech_.trigger('loadstart'); 137 this.tech_.trigger('loadstart');
138 }.bind(this), 1); 138 }.bind(this), 1);
139 139
...@@ -180,6 +180,8 @@ videojs.Hls.prototype.src = function(src) { ...@@ -180,6 +180,8 @@ videojs.Hls.prototype.src = function(src) {
180 }); 180 });
181 } 181 }
182 182
183 this.setupSourceBuffer_();
184
183 selectedPlaylist = this.selectPlaylist(); 185 selectedPlaylist = this.selectPlaylist();
184 oldBitrate = oldMediaPlaylist.attributes && 186 oldBitrate = oldMediaPlaylist.attributes &&
185 oldMediaPlaylist.attributes.BANDWIDTH || 0; 187 oldMediaPlaylist.attributes.BANDWIDTH || 0;
...@@ -281,16 +283,7 @@ videojs.Hls.getMediaIndexForLive_ = function(selectedPlaylist) { ...@@ -281,16 +283,7 @@ videojs.Hls.getMediaIndexForLive_ = function(selectedPlaylist) {
281 }; 283 };
282 284
283 videojs.Hls.prototype.handleSourceOpen = function() { 285 videojs.Hls.prototype.handleSourceOpen = function() {
284 this.sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); 286 this.setupSourceBuffer_();
285
286 // transition the sourcebuffer to the ended state if we've hit the end of
287 // the playlist
288 this.sourceBuffer.addEventListener('updateend', function() {
289 if (this.duration() !== Infinity &&
290 this.mediaIndex === this.playlists.media().segments.length) {
291 this.mediaSource.endOfStream();
292 }
293 }.bind(this));
294 287
295 // if autoplay is enabled, begin playback. This is duplicative of 288 // if autoplay is enabled, begin playback. This is duplicative of
296 // code in video.js but is required because play() must be invoked 289 // code in video.js but is required because play() must be invoked
...@@ -303,6 +296,31 @@ videojs.Hls.prototype.handleSourceOpen = function() { ...@@ -303,6 +296,31 @@ videojs.Hls.prototype.handleSourceOpen = function() {
303 } 296 }
304 }; 297 };
305 298
299 videojs.Hls.prototype.setupSourceBuffer_ = function() {
300 var media = this.playlists.media(), mimeType;
301
302 // wait until a media playlist is available and the Media Source is
303 // attached
304 if (!media || this.mediaSource.readyState !== 'open') {
305 return;
306 }
307
308 mimeType = 'video/mp2t';
309 if (media.attributes && media.attributes.CODECS) {
310 mimeType += '; codecs="' + media.attributes.CODECS + '"';
311 }
312 this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType);
313
314 // transition the sourcebuffer to the ended state if we've hit the end of
315 // the playlist
316 this.sourceBuffer.addEventListener('updateend', function() {
317 if (this.duration() !== Infinity &&
318 this.mediaIndex === this.playlists.media().segments.length) {
319 this.mediaSource.endOfStream();
320 }
321 }.bind(this));
322 };
323
306 // register event listeners to transform in-band metadata events into 324 // register event listeners to transform in-band metadata events into
307 // VTTCues on a text track 325 // VTTCues on a text track
308 videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { 326 videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
...@@ -310,12 +328,6 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { ...@@ -310,12 +328,6 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
310 metadataStream = this.segmentParser_.metadataStream, 328 metadataStream = this.segmentParser_.metadataStream,
311 textTrack; 329 textTrack;
312 330
313 // only expose metadata tracks to video.js versions that support
314 // dynamic text tracks (4.12+)
315 if (!this.tech_.addTextTrack) {
316 return;
317 }
318
319 // add a metadata cue whenever a metadata event is triggered during 331 // add a metadata cue whenever a metadata event is triggered during
320 // segment parsing 332 // segment parsing
321 metadataStream.on('data', function(metadata) { 333 metadataStream.on('data', function(metadata) {
...@@ -585,6 +597,7 @@ videojs.Hls.prototype.dispose = function() { ...@@ -585,6 +597,7 @@ videojs.Hls.prototype.dispose = function() {
585 } 597 }
586 598
587 this.resetSrc_(); 599 this.resetSrc_();
600 Component.prototype.dispose.call(this);
588 }; 601 };
589 602
590 /** 603 /**
......
...@@ -207,7 +207,7 @@ ...@@ -207,7 +207,7 @@
207 'the title is parsed'); 207 'the title is parsed');
208 }); 208 });
209 test('parses #EXTINF tags with carriage returns', function() { 209 test('parses #EXTINF tags with carriage returns', function() {
210 var 210 var
211 manifest = '#EXTINF:13,Does anyone really use the title attribute?\r\n', 211 manifest = '#EXTINF:13,Does anyone really use the title attribute?\r\n',
212 element; 212 element;
213 parseStream.on('data', function(elem) { 213 parseStream.on('data', function(elem) {
...@@ -480,6 +480,16 @@ ...@@ -480,6 +480,16 @@
480 strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf'); 480 strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
481 strictEqual(element.attributes.RESOLUTION.width, 396, 'width is parsed'); 481 strictEqual(element.attributes.RESOLUTION.width, 396, 'width is parsed');
482 strictEqual(element.attributes.RESOLUTION.height, 224, 'heigth is parsed'); 482 strictEqual(element.attributes.RESOLUTION.height, 224, 'heigth is parsed');
483
484 manifest = '#EXT-X-STREAM-INF:CODECS="avc1.4d400d, mp4a.40.2"\n';
485 lineStream.push(manifest);
486
487 ok(element, 'an event was triggered');
488 strictEqual(element.type, 'tag', 'the line type is tag');
489 strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
490 strictEqual(element.attributes.CODECS,
491 'avc1.4d400d, mp4a.40.2',
492 'codecs are parsed');
483 }); 493 });
484 test('parses #EXT-X-STREAM-INF with arbitrary attributes', function() { 494 test('parses #EXT-X-STREAM-INF with arbitrary attributes', function() {
485 var 495 var
......
...@@ -212,7 +212,8 @@ var ...@@ -212,7 +212,8 @@ var
212 212
213 // return an absolute version of a page-relative URL 213 // return an absolute version of a page-relative URL
214 absoluteUrl = function(relativeUrl) { 214 absoluteUrl = function(relativeUrl) {
215 return window.location.origin + 215 return window.location.protocol + '//' +
216 window.location.host +
216 (window.location.pathname 217 (window.location.pathname
217 .split('/') 218 .split('/')
218 .slice(0, -1) 219 .slice(0, -1)
...@@ -239,6 +240,9 @@ module('HLS', { ...@@ -239,6 +240,9 @@ module('HLS', {
239 el.className = 'vjs-tech vjs-mock-flash'; 240 el.className = 'vjs-tech vjs-mock-flash';
240 el.vjs_load = function() {}; 241 el.vjs_load = function() {};
241 el.vjs_getProperty = function(attr) { 242 el.vjs_getProperty = function(attr) {
243 if (attr === 'buffered') {
244 return [[0,0]];
245 }
242 return el[attr]; 246 return el[attr];
243 }; 247 };
244 el.vjs_setProperty = function(attr, value) { 248 el.vjs_setProperty = function(attr, value) {
...@@ -383,6 +387,26 @@ test('duration is set when the source opens after the playlist is loaded', funct ...@@ -383,6 +387,26 @@ test('duration is set when the source opens after the playlist is loaded', funct
383 equal(player.tech.hls.mediaSource.duration , 40, 'set the duration'); 387 equal(player.tech.hls.mediaSource.duration , 40, 'set the duration');
384 }); 388 });
385 389
390 test('codecs are passed to the source buffer', function() {
391 var codecs = [];
392 player.src({
393 src: 'custom-codecs.m3u8',
394 type: 'application/vnd.apple.mpegurl'
395 });
396 openMediaSource(player);
397 player.tech.hls.mediaSource.addSourceBuffer = function(codec) {
398 codecs.push(codec);
399 };
400
401 requests.shift().respond(200, null,
402 '#EXTM3U\n' +
403 '#EXT-X-STREAM-INF:CODECS="video, audio"\n' +
404 'media.m3u8\n');
405 standardXHRResponse(requests.shift());
406 equal(codecs.length, 1, 'created a source buffer');
407 equal(codecs[0], 'video/mp2t; codecs="video, audio"', 'specified the codecs');
408 });
409
386 test('including HLS as a tech does not error', function() { 410 test('including HLS as a tech does not error', function() {
387 var player = createPlayer({ 411 var player = createPlayer({
388 techOrder: ['hls', 'html5'] 412 techOrder: ['hls', 'html5']
...@@ -450,14 +474,14 @@ test('re-initializes the playlist loader when switching sources', function() { ...@@ -450,14 +474,14 @@ test('re-initializes the playlist loader when switching sources', function() {
450 474
451 test('sets the duration if one is available on the playlist', function() { 475 test('sets the duration if one is available on the playlist', function() {
452 var events = 0; 476 var events = 0;
453 player.on('durationchange', function() {
454 events++;
455 });
456 player.src({ 477 player.src({
457 src: 'manifest/media.m3u8', 478 src: 'manifest/media.m3u8',
458 type: 'application/vnd.apple.mpegurl' 479 type: 'application/vnd.apple.mpegurl'
459 }); 480 });
460 openMediaSource(player); 481 openMediaSource(player);
482 player.tech.on('durationchange', function() {
483 events++;
484 });
461 485
462 standardXHRResponse(requests[0]); 486 standardXHRResponse(requests[0]);
463 equal(player.tech.hls.mediaSource.duration, 40, 'set the duration'); 487 equal(player.tech.hls.mediaSource.duration, 40, 'set the duration');
...@@ -549,7 +573,8 @@ test('recognizes domain-relative URLs', function() { ...@@ -549,7 +573,8 @@ test('recognizes domain-relative URLs', function() {
549 standardXHRResponse(requests[0]); 573 standardXHRResponse(requests[0]);
550 standardXHRResponse(requests[1]); 574 standardXHRResponse(requests[1]);
551 strictEqual(requests[1].url, 575 strictEqual(requests[1].url,
552 window.location.origin + '/00001.ts', 576 window.location.protocol + '//' + window.location.host +
577 '/00001.ts',
553 'the first segment is requested'); 578 'the first segment is requested');
554 }); 579 });
555 580
...@@ -565,11 +590,11 @@ test('re-initializes the handler for each source', function() { ...@@ -565,11 +590,11 @@ test('re-initializes the handler for each source', function() {
565 openMediaSource(player); 590 openMediaSource(player);
566 firstPlaylists = player.tech.hls.playlists; 591 firstPlaylists = player.tech.hls.playlists;
567 firstMSE = player.tech.hls.mediaSource; 592 firstMSE = player.tech.hls.mediaSource;
593 standardXHRResponse(requests.shift());
594 standardXHRResponse(requests.shift());
568 player.tech.hls.sourceBuffer.abort = function() { 595 player.tech.hls.sourceBuffer.abort = function() {
569 aborts++; 596 aborts++;
570 }; 597 };
571 standardXHRResponse(requests.shift());
572 standardXHRResponse(requests.shift());
573 598
574 player.src({ 599 player.src({
575 src: 'manifest/master.m3u8', 600 src: 'manifest/master.m3u8',
...@@ -1329,9 +1354,11 @@ test('when outstanding XHRs are cancelled, they get aborted properly', function( ...@@ -1329,9 +1354,11 @@ test('when outstanding XHRs are cancelled, they get aborted properly', function(
1329 test('segmentXhr is properly nulled out when dispose is called', function() { 1354 test('segmentXhr is properly nulled out when dispose is called', function() {
1330 var 1355 var
1331 readystatechanges = 0, 1356 readystatechanges = 0,
1332 oldDispose = Flash.prototype.dispose; 1357 oldDispose = Flash.prototype.dispose,
1358 player;
1333 Flash.prototype.dispose = function() {}; 1359 Flash.prototype.dispose = function() {};
1334 1360
1361 player = createPlayer();
1335 player.src({ 1362 player.src({
1336 src: 'manifest/media.m3u8', 1363 src: 'manifest/media.m3u8',
1337 type: 'application/vnd.apple.mpegurl' 1364 type: 'application/vnd.apple.mpegurl'
...@@ -2108,7 +2135,7 @@ test('does not break if the playlist has no segments', function() { ...@@ -2108,7 +2135,7 @@ test('does not break if the playlist has no segments', function() {
2108 }); 2135 });
2109 2136
2110 test('clears the segment buffer on seek', function() { 2137 test('clears the segment buffer on seek', function() {
2111 var currentTime, bufferEnd, oldCurrentTime; 2138 var currentTime, oldCurrentTime;
2112 2139
2113 player.src({ 2140 player.src({
2114 src: 'discontinuity.m3u8', 2141 src: 'discontinuity.m3u8',
...@@ -2122,8 +2149,8 @@ test('clears the segment buffer on seek', function() { ...@@ -2122,8 +2149,8 @@ test('clears the segment buffer on seek', function() {
2122 } 2149 }
2123 return currentTime; 2150 return currentTime;
2124 }; 2151 };
2125 player.buffered = function() { 2152 player.tech.buffered = function() {
2126 return videojs.createTimeRange(0, bufferEnd); 2153 return videojs.createTimeRange();
2127 }; 2154 };
2128 2155
2129 requests.pop().respond(200, null, 2156 requests.pop().respond(200, null,
...@@ -2139,7 +2166,6 @@ test('clears the segment buffer on seek', function() { ...@@ -2139,7 +2166,6 @@ test('clears the segment buffer on seek', function() {
2139 2166
2140 // play to 6s to trigger the next segment request 2167 // play to 6s to trigger the next segment request
2141 currentTime = 6; 2168 currentTime = 6;
2142 bufferEnd = 10;
2143 clock.tick(6000); 2169 clock.tick(6000);
2144 2170
2145 standardXHRResponse(requests.pop()); // 2.ts 2171 standardXHRResponse(requests.pop()); // 2.ts
...@@ -2346,6 +2372,17 @@ test('aborts the source buffer on disposal', function() { ...@@ -2346,6 +2372,17 @@ test('aborts the source buffer on disposal', function() {
2346 type: 'application/vnd.apple.mpegurl' 2372 type: 'application/vnd.apple.mpegurl'
2347 }); 2373 });
2348 openMediaSource(player); 2374 openMediaSource(player);
2375 player.dispose();
2376 ok(true, 'disposed before creating the source buffer');
2377 requests.length = 0;
2378
2379 player = createPlayer();
2380 player.src({
2381 src: 'manifest/media.m3u8',
2382 type: 'application/vnd.apple.mpegurl'
2383 });
2384 openMediaSource(player);
2385 standardXHRResponse(requests.shift());
2349 player.tech.hls.sourceBuffer.abort = function() { 2386 player.tech.hls.sourceBuffer.abort = function() {
2350 aborts++; 2387 aborts++;
2351 }; 2388 };
...@@ -2708,9 +2745,6 @@ test('skip segments if key requests fail more than once', function() { ...@@ -2708,9 +2745,6 @@ test('skip segments if key requests fail more than once', function() {
2708 type: 'application/vnd.apple.mpegurl' 2745 type: 'application/vnd.apple.mpegurl'
2709 }); 2746 });
2710 openMediaSource(player); 2747 openMediaSource(player);
2711 player.tech.hls.sourceBuffer.appendBuffer = function(chunk) {
2712 bytes.push(chunk);
2713 };
2714 player.tech.trigger('play'); 2748 player.tech.trigger('play');
2715 2749
2716 requests.shift().respond(200, null, 2750 requests.shift().respond(200, null,
...@@ -2721,6 +2755,9 @@ test('skip segments if key requests fail more than once', function() { ...@@ -2721,6 +2755,9 @@ test('skip segments if key requests fail more than once', function() {
2721 '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' + 2755 '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' +
2722 '#EXTINF:15.0,\n' + 2756 '#EXTINF:15.0,\n' +
2723 'http://media.example.com/fileSequence53-A.ts\n'); 2757 'http://media.example.com/fileSequence53-A.ts\n');
2758 player.tech.hls.sourceBuffer.appendBuffer = function(chunk) {
2759 bytes.push(chunk);
2760 };
2724 standardXHRResponse(requests.shift()); // segment 1 2761 standardXHRResponse(requests.shift()); // segment 1
2725 requests.shift().respond(404); // fail key 2762 requests.shift().respond(404); // fail key
2726 requests.shift().respond(404); // fail key, again 2763 requests.shift().respond(404); // fail key, again
...@@ -2878,9 +2915,6 @@ test('treats invalid keys as a key request failure', function() { ...@@ -2878,9 +2915,6 @@ test('treats invalid keys as a key request failure', function() {
2878 type: 'application/vnd.apple.mpegurl' 2915 type: 'application/vnd.apple.mpegurl'
2879 }); 2916 });
2880 openMediaSource(player); 2917 openMediaSource(player);
2881 player.tech.hls.sourceBuffer.appendBuffer = function(chunk) {
2882 bytes.push(chunk);
2883 };
2884 player.tech.trigger('play'); 2918 player.tech.trigger('play');
2885 requests.shift().respond(200, null, 2919 requests.shift().respond(200, null,
2886 '#EXTM3U\n' + 2920 '#EXTM3U\n' +
...@@ -2891,6 +2925,9 @@ test('treats invalid keys as a key request failure', function() { ...@@ -2891,6 +2925,9 @@ test('treats invalid keys as a key request failure', function() {
2891 '#EXT-X-KEY:METHOD=NONE\n' + 2925 '#EXT-X-KEY:METHOD=NONE\n' +
2892 '#EXTINF:15.0,\n' + 2926 '#EXTINF:15.0,\n' +
2893 'http://media.example.com/fileSequence52-B.ts\n'); 2927 'http://media.example.com/fileSequence52-B.ts\n');
2928 player.tech.hls.sourceBuffer.appendBuffer = function(chunk) {
2929 bytes.push(chunk);
2930 };
2894 // segment request 2931 // segment request
2895 standardXHRResponse(requests.shift()); 2932 standardXHRResponse(requests.shift());
2896 // keys should be 16 bytes long 2933 // keys should be 16 bytes long
......