Skip to content

Commit

Permalink
Better Safari caching workaround, and skip the HEAD request
Browse files Browse the repository at this point in the history
Uses Content-Range response header in the first 206 to get the
total byte size instead of a separate HEAD request.

Changed workaround for Safari range caching bug to only trigger
if we detect a bad range, and then use a consistent query parameter
until we get another bad one.

This seems to reduce unnecessary cache churn, as the bug seems most
likely to trigger when reaching the end of the file and then going
back, rather than when going forward.
  • Loading branch information
bvibber committed Nov 7, 2014
1 parent 27c7763 commit 9ab5bce
Showing 1 changed file with 78 additions and 50 deletions.
128 changes: 78 additions & 50 deletions src/StreamFile.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ function StreamFile(options) {
bytesTotal = 0,
bytesRead = 0,
buffers = [],
cachever = 0,
responseHeaders = {};


Expand Down Expand Up @@ -63,12 +64,17 @@ function StreamFile(options) {
},

setBytesTotal: function(xhr) {
var contentLength = xhr.getResponseHeader('Content-Length');
if (contentLength == null || contentLength == '') {
// Unknown file length... maybe streaming live?
bytesTotal = 0;
if (xhr.status == 206) {
bytesTotal = internal.getXHRRangeTotal(xhr);
console.log('Total file size is ' + bytesTotal);
} else {
bytesTotal = parseInt(contentLength, 10);
var contentLength = xhr.getResponseHeader('Content-Length');
if (contentLength == null || contentLength == '') {
// Unknown file length... maybe streaming live?
bytesTotal = 0;
} else {
bytesTotal = parseInt(contentLength, 10);
}
}
},

Expand All @@ -86,58 +92,21 @@ function StreamFile(options) {
}
});
},

getMetadata: function() {
var xhr = new XMLHttpRequest();
var headUrl = url;

if (navigator.userAgent.match(/Chrome/)) {
// HACK ALERT!
//
// Cache buster because cached HEAD reqs in Chrome don't
// include a Content-Length header, which defeats the purpose.
// And we're not allowed to send cache-control. Oh CORS!
//
// Better would be to get the Content-Range from the 206
// response, but it's not whitelisted yet. *headdesk*
//
headUrl += '?ogvjs_cachebuster=' + Math.random()
} else if (navigator.userAgent.match(/AppleWebKit/)) {
//
// Safari sometimes messes up and gives us the Content-Length
// from the final-seeking chunk instead of the whole file. What?
// Seems to be a general problem with Safari and cached XHR ranges.
//
// https://bugs.webkit.org/show_bug.cgi?id=82672
//
headUrl += '?ogvjs_cacherange=HEAD';
}

xhr.open("HEAD", headUrl);

xhr.onreadystatechange = function(event) {
if (xhr.readyState == 2) {
internal.onXHRHeadersReceived(xhr);
} else if (xhr.readyState == 4) {
xhr.onreadystatechange = null;
xhr = null;
internal.openXHR();
}
};
xhr.send();
},

openXHR: function() {
var getUrl = url;
if (navigator.userAgent.match(/AppleWebKit/)) {
// HACK ALERT!
if (cachever) {
//
// Safari sometimes messes up and gives us the wrong chunk.
// Seems to be a general problem with Safari and cached XHR ranges.
//
// Interestingly, it allows you to request _later_ ranges successfully,
// but when requesting _earlier_ ranges it returns the latest one retrieved.
// So we only need to update the cache-buster when we rewind.
//
// https://bugs.webkit.org/show_bug.cgi?id=82672
//
getUrl += '?ogvjs_cacherange=' + seekPosition + '-' + (chunkSize ? (seekPosition + chunkSize - 1) : '');
getUrl += '?ogvjs_cachever=' + cachever;
}

var xhr = internal.xhr = new XMLHttpRequest();
Expand All @@ -150,7 +119,11 @@ function StreamFile(options) {
range = 'bytes=' + seekPosition + '-';
}
if (chunkSize) {
range += Math.min(seekPosition + chunkSize, bytesTotal) - 1;
if (bytesTotal) {
range += Math.min(seekPosition + chunkSize, bytesTotal) - 1;
} else {
range += (seekPosition + chunkSize) - 1;
}
}
if (range !== null) {
xhr.setRequestHeader('Range', range);
Expand All @@ -160,6 +133,34 @@ function StreamFile(options) {

xhr.onreadystatechange = function(event) {
if (xhr.readyState == 2) {
if (xhr.status == 206) {
var foundPosition = internal.getXHRRangeStart(xhr);
if (seekPosition != foundPosition) {
//
// Safari sometimes messes up and gives us the wrong chunk.
// Seems to be a general problem with Safari and cached XHR ranges.
//
// Interestingly, it allows you to request _later_ ranges successfully,
// but when requesting _earlier_ ranges it returns the latest one retrieved.
// So we only need to update the cache-buster when we rewind and actually
// get an incorrect range.
//
// https://bugs.webkit.org/show_bug.cgi?id=82672
//
console.log('Expected start at ' + seekPosition + ' but got ' + foundPosition +
'; working around Safari range caching bug: https://bugs.webkit.org/show_bug.cgi?id=82672');
cachever++;
internal.abortXHR(xhr);
internal.openXHR();
return;
}
}
if (!started) {
internal.setBytesTotal(xhr);
internal.processResponseHeaders(xhr);
started = true;
onstart();
}
//internal.onXHRHeadersReceived(xhr);
// @todo check that partial content was supported if relevant
} else if (xhr.readyState == 3) {
Expand All @@ -173,10 +174,36 @@ function StreamFile(options) {
xhr.send();
},

getXHRRangeMatches: function(xhr) {
// Note Content-Range must be whitelisted for CORS requests
var contentRange = xhr.getResponseHeader('Content-Range');
console.log(contentRange);
return contentRange && contentRange.match(/^bytes (\d+)-(\d+)\/(\d+)/);
},

getXHRRangeStart: function(xhr) {
var matches = internal.getXHRRangeMatches(xhr);
if (matches) {
return parseInt(matches[1], 10);
} else {
return 0;
}
},

getXHRRangeTotal: function(xhr) {
var matches = internal.getXHRRangeMatches(xhr);
if (matches) {
return parseInt(matches[3], 10);
} else {
return 0;
}
},

setXHROptions: function(xhr) {
throw new Error('abstract function');
},

/*
onXHRHeadersReceived: function(xhr) {
if (xhr.status >= 400) {
// errrorrrrrrr
Expand All @@ -190,6 +217,7 @@ function StreamFile(options) {
onstart();
}
},
*/

onXHRLoading: function(xhr) {
throw new Error('abstract function');
Expand Down Expand Up @@ -533,5 +561,5 @@ function StreamFile(options) {
throw new Error("No streaming HTTP input method found.");
}

internal.getMetadata();
internal.openXHR();
}

0 comments on commit 9ab5bce

Please sign in to comment.