Bug 522114. While we're suspending the download, a Web server might close our connection, thinking we're dead. This might look just like a normal connection close. So whenever the connection closes after we resumed, try to reopen it, if we're seekable. r=kinetik
authorRobert O'Callahan <robert@ocallahan.org>
Thu, 15 Oct 2009 15:20:49 +1300
changeset 32749 aeb61beac3739a6a6dc410a0f5b05ad6ba1f2dc4
parent 32748 efb34f0db626de6a05af160dec3c3b170683576e
child 32750 8755440fb2a426309bf4e6b7c6ccf91c66a6db96
push id577
push userrocallahan@mozilla.com
push dateThu, 05 Nov 2009 07:06:48 +0000
reviewerskinetik
bugs522114
milestone1.9.2b2pre
Bug 522114. While we're suspending the download, a Web server might close our connection, thinking we're dead. This might look just like a normal connection close. So whenever the connection closes after we resumed, try to reopen it, if we're seekable. r=kinetik
content/media/nsMediaStream.cpp
content/media/test/Makefile.in
content/media/test/cancellable_request.sjs
content/media/test/test_resume.html
--- a/content/media/nsMediaStream.cpp
+++ b/content/media/nsMediaStream.cpp
@@ -281,18 +281,28 @@ nsMediaChannelStream::OnStopRequest(nsIR
   NS_ASSERTION(mSuspendCount == 0,
                "How can OnStopRequest fire while we're suspended?");
 
   {
     nsAutoLock lock(mLock);
     mChannelStatistics.Stop(TimeStamp::Now());
   }
 
-  if (NS_FAILED(aStatus) && aStatus != NS_ERROR_PARSED_DATA_CACHED &&
-      mReopenOnError) {
+  // Note that aStatus might have succeeded --- this might be a normal close
+  // --- even in situations where the server cut us off because we were
+  // suspended. So we need to "reopen on error" in that case too. The only
+  // cases where we don't need to reopen are when *we* closed the stream.
+  // But don't reopen if we need to seek and we don't think we can... that would
+  // cause us to just re-read the stream, which would be really bad.
+  if (mReopenOnError &&
+      aStatus != NS_ERROR_PARSED_DATA_CACHED && aStatus != NS_BINDING_ABORTED &&
+      (mOffset == 0 || mCacheStream.IsSeekable())) {
+    // If the stream did close normally, then if the server is seekable we'll
+    // just seek to the end of the resource and get an HTTP 416 error because
+    // there's nothing there, so this isn't bad.
     nsresult rv = CacheClientSeek(mOffset, PR_FALSE);
     if (NS_SUCCEEDED(rv))
       return rv;
     // If the reopen/reseek fails, just fall through and treat this
     // error as fatal.
   }
 
   if (!mIgnoreClose) {
--- a/content/media/test/Makefile.in
+++ b/content/media/test/Makefile.in
@@ -64,16 +64,17 @@ include $(topsrcdir)/config/rules.mk
 
 # To test for a specific bug in handling a specific resource type,
 # make the test first check canPlayType for the type, and if it's not
 # supported, just do ok(true, "Type not supported") and stop the test.
 
 _TEST_FILES = \
 		can_play_type_ogg.js \
 		can_play_type_wave.js \
+		cancellable_request.sjs \
 		manifest.js \
 		seek1.js \
 		seek2.js \
 		seek3.js \
 		seek4.js \
 		seek5.js \
 		seek6.js \
 		seek7.js \
@@ -94,16 +95,17 @@ include $(topsrcdir)/config/rules.mk
 		test_load.html \
 		test_media_selection.html \
 		test_mozLoadFrom.html \
 		test_networkState.html \
 		test_paused.html \
 		test_playback.html \
 		test_playback_errors.html \
 		test_readyState.html \
+		test_resume.html \
 		test_seek2.html \
 		test_volume.html \
 		use_large_cache.js \
 		$(NULL)
 
 ifneq ($(OS_ARCH),WINNT)
 # These tests are disabled on windows until we
 # figure out the random failures. See bug 475369.
new file mode 100644
--- /dev/null
+++ b/content/media/test/cancellable_request.sjs
@@ -0,0 +1,140 @@
+function push32BE(array, input) {
+  array.push(String.fromCharCode((input >> 24) & 0xff));
+  array.push(String.fromCharCode((input >> 16) & 0xff));
+  array.push(String.fromCharCode((input >> 8) & 0xff));
+  array.push(String.fromCharCode((input) & 0xff));
+}
+
+function push32LE(array, input) {
+  array.push(String.fromCharCode((input) & 0xff));
+  array.push(String.fromCharCode((input >> 8) & 0xff));
+  array.push(String.fromCharCode((input >> 16) & 0xff));
+  array.push(String.fromCharCode((input >> 24) & 0xff));
+}
+
+function push16LE(array, input) {
+  array.push(String.fromCharCode((input) & 0xff));
+  array.push(String.fromCharCode((input >> 8) & 0xff));
+}
+
+function buildWave(samples, sample_rate) {
+  const RIFF_MAGIC = 0x52494646;
+  const WAVE_MAGIC = 0x57415645;
+  const FRMT_MAGIC = 0x666d7420;
+  const DATA_MAGIC = 0x64617461;
+  const RIFF_SIZE = 44;
+
+  var header = [];
+  push32BE(header, RIFF_MAGIC);
+  push32LE(header, RIFF_SIZE + samples.length * 2);
+  push32BE(header, WAVE_MAGIC);
+  push32BE(header, FRMT_MAGIC);
+  push32LE(header, 16);
+  push16LE(header, 1);
+  push16LE(header, 1);
+  push32LE(header, sample_rate);
+  push32LE(header, sample_rate);
+  push16LE(header, 2);
+  push16LE(header, 16);
+  push32BE(header, DATA_MAGIC);
+  push32LE(header, samples.length * 2);
+  for (var i = 0; i < samples.length; ++i) {
+    push16LE(header, samples[i], 2);
+  }
+  return header;
+}
+
+const Ci = Components.interfaces;
+const CC = Components.Constructor;
+const Timer = CC("@mozilla.org/timer;1", "nsITimer", "initWithCallback");
+const BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1",
+                             "nsIBinaryOutputStream",
+                             "setOutputStream");
+
+function poll(f) {
+  if (f()) {
+    return;
+  }
+  new Timer(function() { poll(f); }, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+}
+
+function handleRequest(request, response)
+{
+  var cancel = request.queryString.match(/^cancelkey=(.*)$/);
+  if (cancel) {
+    setState(cancel[1], "cancelled");
+    response.setStatusLine(request.httpVersion, 200, "OK");
+    response.write("Cancel approved!");
+    return;
+  }
+
+  var samples = [];
+  for (var i = 0; i < 100000; ++i) {
+    samples.push(0);
+  }
+  var bytes = buildWave(samples, 44100).join("");
+
+  var key = request.queryString.match(/^key=(.*)$/);
+  response.setHeader("Content-Type", "audio/x-wav");
+  response.setHeader("Content-Length", ""+bytes.length, false);
+
+  var out = new BinaryOutputStream(response.bodyOutputStream);
+
+  var start = 0, end = bytes.length - 1;
+  if (request.hasHeader("Range"))
+  {
+    var rangeMatch = request.getHeader("Range").match(/^bytes=(\d+)?-(\d+)?$/);
+
+    if (rangeMatch[1] !== undefined)
+      start = parseInt(rangeMatch[1], 10);
+
+    if (rangeMatch[2] !== undefined)
+      end = parseInt(rangeMatch[2], 10);
+
+    // No start given, so the end is really the count of bytes from the
+    // end of the file.
+    if (start === undefined)
+    {
+      start = Math.max(0, bytes.length - end);
+      end   = bytes.length - 1;
+    }
+
+    // start and end are inclusive
+    if (end === undefined || end >= bytes.length)
+      end = bytes.length - 1;
+
+    if (end < start)
+    {
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      start = 0;
+      end = bytes.length - 1;
+    }
+    else
+    {
+      response.setStatusLine(request.httpVersion, 206, "Partial Content");
+      var contentRange = "bytes " + start + "-" + end + "/" + bytes.length;
+      response.setHeader("Content-Range", contentRange);
+    }
+  }
+  
+  if (start > 0) {
+    // Send all requested data
+    out.write(bytes.slice(start, end + 1), end + 1 - start);
+    return;
+  }
+
+  // Write the first 120K of the Wave file. We know the cache size is set to
+  // 100K so this will fill the cache and and cause a "suspend" event on
+  // the loading element.
+  out.write(bytes, 120000);
+
+  response.processAsync();
+  // Now wait for the message to cancel this response
+  poll(function() {
+    if (getState(key[1]) != "cancelled") {
+      return false;
+    }
+    response.finish();
+    return true;
+  });
+}
new file mode 100644
--- /dev/null
+++ b/content/media/test/test_resume.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Media test: Test resume of server-dropped connections</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<audio autobuffer id="a"></audio>
+<iframe id="f"></iframe>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var key = Math.round(Math.random()*1000000000);
+var a = document.getElementById("a");
+var f = document.getElementById("f");
+
+function didEnd() {
+  ok(a.currentTime > 2.26, "Reached correct end time (got " + a.currentTime + ", expected > 2.26");
+  SimpleTest.finish();
+}
+
+function didSendCancel() {
+  a.addEventListener("ended", didEnd, false);
+  a.play();
+}
+
+function didSuspend() {
+  a.removeEventListener("suspend", didSuspend, false);
+
+  // Cache must have filled up, or something. Tell the Web server to drop
+  // our connection.
+  f.addEventListener("load", didSendCancel, false);
+  f.src = "cancellable_request.sjs?cancelkey=" + key;
+}
+
+if (!a.canPlayType("audio/wave")) {
+  todo(false, "Test requires support for audio/wave");
+} else {
+  a.addEventListener("suspend", didSuspend, false);
+  a.src = "cancellable_request.sjs?key=" + key;
+  SimpleTest.waitForExplicitFinish();
+}
+</script>
+</pre>
+</body>
+</html>