Bug 462957 - Ogg support for HTMLMediaElement.buffered. r=doublec a=blocking2.0
authorChris Pearce <chris@pearce.org.nz>
Thu, 05 Aug 2010 19:40:35 +1200
changeset 48904 f0167f09b3ba74d6eefce849c70b70c41888cd3b
parent 48903 225b2a943f2cda7aaa72e7398f4fc0033f579515
child 48905 6631295efdce97725ba4022a41ba4b37309982a4
push idunknown
push userunknown
push dateunknown
reviewersdoublec, blocking2
bugs462957
milestone2.0b4pre
Bug 462957 - Ogg support for HTMLMediaElement.buffered. r=doublec a=blocking2.0
content/media/nsBuiltinDecoderReader.h
content/media/nsBuiltinDecoderStateMachine.h
content/media/nsMediaStream.cpp
content/media/nsMediaStream.h
content/media/ogg/nsOggReader.cpp
content/media/ogg/nsOggReader.h
--- a/content/media/nsBuiltinDecoderReader.h
+++ b/content/media/nsBuiltinDecoderReader.h
@@ -422,19 +422,19 @@ public:
   virtual PRBool HasAudio() = 0;
   virtual PRBool HasVideo() = 0;
 
   // Read header data for all bitstreams in the file. Fills mInfo with
   // the data required to present the media. Returns NS_OK on success,
   // or NS_ERROR_FAILURE on failure.
   virtual nsresult ReadMetadata() = 0;
 
-
-  // Stores the presentation time of the first sample in the stream in
-  // aOutStartTime, and returns the first video sample, if we have video.
+  // Stores the presentation time of the first frame/sample we'd be
+  // able to play if we started playback at aOffset, and returns the
+  // first video sample, if we have video.
   virtual VideoData* FindStartTime(PRInt64 aOffset,
                                    PRInt64& aOutStartTime);
 
   // Returns the end time of the last page which occurs before aEndOffset.
   // This will not read past aEndOffset. Returns -1 on failure. 
   virtual PRInt64 FindEndTime(PRInt64 aEndOffset);
 
   // Moves the decode head to aTime milliseconds. aStartTime and aEndTime
@@ -447,18 +447,22 @@ public:
   }
 
   // Queue of audio samples. This queue is threadsafe.
   MediaQueue<SoundData> mAudioQueue;
 
   // Queue of video samples. This queue is threadsafe.
   MediaQueue<VideoData> mVideoQueue;
 
-  // This is called on the main thread, and it must not block.
-  virtual nsresult GetBuffered(nsHTMLTimeRanges* aBuffered, PRInt64 aStartTime) = 0;
+  // Populates aBuffered with the time ranges which are buffered. aStartTime
+  // must be the presentation time of the first sample/frame in the media, e.g.
+  // the media time corresponding to playback time/position 0. This function
+  // should only be called on the main thread.
+  virtual nsresult GetBuffered(nsHTMLTimeRanges* aBuffered,
+                               PRInt64 aStartTime) = 0;
 
 protected:
 
   // Reader decode function. Matches DecodeVideoFrame() and
   // DecodeAudioData().
   typedef PRBool (nsBuiltinDecoderReader::*DecodeFn)();
 
   // Calls aDecodeFn on *this until aQueue has a sample, whereupon
--- a/content/media/nsBuiltinDecoderStateMachine.h
+++ b/content/media/nsBuiltinDecoderStateMachine.h
@@ -230,16 +230,17 @@ public:
 
   // The decoder monitor must be obtained before modifying this state.
   // NotifyAll on the monitor must be called when the state is changed by
   // the main thread so the decoder thread can wake up.
   // Accessed on state machine, audio, main, and AV thread. 
   State mState;
 
   nsresult GetBuffered(nsHTMLTimeRanges* aBuffered) {
+    NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
     return mReader->GetBuffered(aBuffered, mStartTime);
   }
 
 protected:
 
   // Returns PR_TRUE when there's decoded audio waiting to play.
   // The decoder monitor must be held.
   PRBool HasFutureAudio() const;
--- a/content/media/nsMediaStream.cpp
+++ b/content/media/nsMediaStream.cpp
@@ -551,17 +551,26 @@ void nsMediaChannelStream::CloseChannel(
     // document load to think there was an error.
     // NS_ERROR_PARSED_DATA_CACHED is the best thing we have for that
     // at the moment.
     mChannel->Cancel(NS_ERROR_PARSED_DATA_CACHED);
     mChannel = nsnull;
   }
 }
 
-nsresult nsMediaChannelStream::Read(char* aBuffer, PRUint32 aCount, PRUint32* aBytes)
+nsresult nsMediaChannelStream::ReadFromCache(char* aBuffer,
+                                             PRInt64 aOffset,
+                                             PRUint32 aCount)
+{
+  return mCacheStream.ReadFromCache(aBuffer, aOffset, aCount);
+}
+
+nsresult nsMediaChannelStream::Read(char* aBuffer,
+                                    PRUint32 aCount,
+                                    PRUint32* aBytes)
 {
   NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread");
 
   return mCacheStream.Read(aBuffer, aCount, aBytes);
 }
 
 nsresult nsMediaChannelStream::Seek(PRInt32 aWhence, PRInt64 aOffset)
 {
@@ -849,16 +858,17 @@ public:
 
   // Main thread
   virtual nsresult Open(nsIStreamListener** aStreamListener);
   virtual nsresult Close();
   virtual void     Suspend(PRBool aCloseImmediately) {}
   virtual void     Resume() {}
   virtual already_AddRefed<nsIPrincipal> GetCurrentPrincipal();
   virtual nsMediaStream* CloneData(nsMediaDecoder* aDecoder);
+  virtual nsresult ReadFromCache(char* aBuffer, PRInt64 aOffset, PRUint32 aCount);
 
   // These methods are called off the main thread.
 
   // Other thread
   virtual void     SetReadMode(nsMediaCacheStream::ReadMode aMode) {}
   virtual void     SetPlaybackRate(PRUint32 aBytesPerSecond) {}
   virtual nsresult Read(char* aBuffer, PRUint32 aCount, PRUint32* aBytes);
   virtual nsresult Seek(PRInt32 aWhence, PRInt64 aOffset);
@@ -1027,16 +1037,45 @@ nsMediaStream* nsMediaFileStream::CloneD
   nsresult rv =
     NS_NewChannel(getter_AddRefs(channel), mURI, nsnull, loadGroup, nsnull, 0);
   if (NS_FAILED(rv))
     return nsnull;
 
   return new nsMediaFileStream(aDecoder, channel, mURI);
 }
 
+nsresult nsMediaFileStream::ReadFromCache(char* aBuffer, PRInt64 aOffset, PRUint32 aCount)
+{
+  nsAutoLock lock(mLock);
+  if (!mInput || !mSeekable)
+    return NS_ERROR_FAILURE;
+  PRInt64 offset = 0;
+  nsresult res = mSeekable->Tell(&offset);
+  NS_ENSURE_SUCCESS(res,res);
+  res = mSeekable->Seek(nsISeekableStream::NS_SEEK_SET, aOffset);
+  NS_ENSURE_SUCCESS(res,res);
+  PRUint32 bytesRead = 0;
+  do {
+    PRUint32 x = 0;
+    PRUint32 bytesToRead = aCount - bytesRead;
+    res = mInput->Read(aBuffer, bytesToRead, &x);
+    bytesRead += x;
+  } while (bytesRead != aCount && res == NS_OK);
+
+  // Reset read head to original position so we don't disturb any other
+  // reading thread.
+  nsresult seekres = mSeekable->Seek(nsISeekableStream::NS_SEEK_SET, offset);
+
+  // If a read failed in the loop above, we want to return its failure code.
+  NS_ENSURE_SUCCESS(res,res);
+
+  // Else we succeed if the reset-seek succeeds.
+  return seekres;
+}
+
 nsresult nsMediaFileStream::Read(char* aBuffer, PRUint32 aCount, PRUint32* aBytes)
 {
   nsAutoLock lock(mLock);
   if (!mInput)
     return NS_ERROR_FAILURE;
   return mInput->Read(aBuffer, aCount, aBytes);
 }
 
--- a/content/media/nsMediaStream.h
+++ b/content/media/nsMediaStream.h
@@ -129,21 +129,19 @@ private:
 
 /*
    Provides the ability to open, read and seek into a media stream
    (audio, video). Handles the underlying machinery to do range
    requests, etc as needed by the actual stream type. Instances of
    this class must be created on the main thread. 
 
    Most methods must be called on the main thread only. Read, Seek and
-   Tell may be called on another thread which may be a non main
-   thread. They may not be called on multiple other threads though. In
-   the case of the Ogg Decoder they are called on the Decode thread
-   for example. You must ensure that no threads are calling these
-   methods once Close is called.
+   Tell must only be called on non-main threads. In the case of the Ogg
+   Decoder they are called on the Decode thread for example. You must
+   ensure that no threads are calling these methods once Close is called.
 
    Instances of this class are explicitly managed. 'delete' it when done.
 */
 class nsMediaStream 
 {
 public:
   virtual ~nsMediaStream()
   {
@@ -248,16 +246,24 @@ public:
   // is in cache. If the end of the stream is not known, we return false.
   virtual PRBool IsDataCachedToEndOfStream(PRInt64 aOffset) = 0;
   // Returns true if this stream is suspended by the cache because the
   // cache is full. If true then the decoder should try to start consuming
   // data, otherwise we may not be able to make progress.
   // nsMediaDecoder::NotifySuspendedStatusChanged is called when this
   // changes.
   virtual PRBool IsSuspendedByCache() = 0;
+  // Reads only data which is cached in the media cache. If you try to read
+  // any data which overlaps uncached data, or if aCount bytes otherwise can't
+  // be read, this function will return failure. This function be called from
+  // any thread, and it is the only read operation which is safe to call on
+  // the main thread, since it's guaranteed to be non blocking.
+  virtual nsresult ReadFromCache(char* aBuffer,
+                                 PRInt64 aOffset,
+                                 PRUint32 aCount) = 0;
 
   /**
    * Create a stream, reading data from the media resource via the
    * channel. Call on main thread only.
    * The caller must follow up by calling aStream->Open.
    */
   static nsMediaStream* Create(nsMediaDecoder* aDecoder, nsIChannel* aChannel);
 
@@ -337,16 +343,17 @@ public:
   virtual nsresult Open(nsIStreamListener** aStreamListener);
   virtual nsresult Close();
   virtual void     Suspend(PRBool aCloseImmediately);
   virtual void     Resume();
   virtual already_AddRefed<nsIPrincipal> GetCurrentPrincipal();
   // Return PR_TRUE if the stream has been closed.
   PRBool IsClosed() const { return mCacheStream.IsClosed(); }
   virtual nsMediaStream* CloneData(nsMediaDecoder* aDecoder);
+  virtual nsresult ReadFromCache(char* aBuffer, PRInt64 aOffset, PRUint32 aCount);
 
   // Other thread
   virtual void     SetReadMode(nsMediaCacheStream::ReadMode aMode);
   virtual void     SetPlaybackRate(PRUint32 aBytesPerSecond);
   virtual nsresult Read(char* aBuffer, PRUint32 aCount, PRUint32* aBytes);
   virtual nsresult Seek(PRInt32 aWhence, PRInt64 aOffset);
   virtual PRInt64  Tell();
 
--- a/content/media/ogg/nsOggReader.cpp
+++ b/content/media/ogg/nsOggReader.cpp
@@ -37,16 +37,17 @@
  *
  * ***** END LICENSE BLOCK ***** */
 #include "nsError.h"
 #include "nsBuiltinDecoderStateMachine.h"
 #include "nsBuiltinDecoder.h"
 #include "nsOggReader.h"
 #include "VideoUtils.h"
 #include "theora/theoradec.h"
+#include "nsHTMLTimeRanges.h"
 
 using namespace mozilla;
 
 // Un-comment to enable logging of seek bisections.
 //#define SEEK_LOGGING
 
 #ifdef PR_LOGGING
 extern PRLogModuleInfo* gBuiltinDecoderLog;
@@ -128,17 +129,16 @@ static PRBool DoneReadingHeaders(nsTArra
   for (PRUint32 i = 0; i < aBitstreams .Length(); i++) {
     if (!aBitstreams [i]->DoneReadingHeaders()) {
       return PR_FALSE;
     }
   }
   return PR_TRUE;
 }
 
-
 nsresult nsOggReader::ReadMetadata()
 {
   NS_ASSERTION(mDecoder->OnStateMachineThread(), "Should be on play state machine thread.");
   MonitorAutoEnter mon(mMonitor);
 
   // We read packets until all bitstreams have read all their header packets.
   // We record the offset of the first non-header page so that we know
   // what page to seek to when seeking to the media start.
@@ -792,87 +792,106 @@ GetChecksum(ogg_page* page)
                (p[2] << 16) +
                (p[3] << 24);
   return c;
 }
 
 VideoData* nsOggReader::FindStartTime(PRInt64 aOffset,
                                       PRInt64& aOutStartTime)
 {
-  NS_ASSERTION(mDecoder->OnStateMachineThread(), "Should be on state machine thread.");
-
+  NS_ASSERTION(mDecoder->OnStateMachineThread(),
+               "Should be on state machine thread.");
   nsMediaStream* stream = mDecoder->GetCurrentStream();
-  stream->Seek(nsISeekableStream::NS_SEEK_SET, aOffset);
+  NS_ENSURE_TRUE(stream != nsnull, nsnull);
+  nsresult res = stream->Seek(nsISeekableStream::NS_SEEK_SET, aOffset);
+  NS_ENSURE_SUCCESS(res, nsnull);
   return nsBuiltinDecoderReader::FindStartTime(aOffset, aOutStartTime);
 }
 
 PRInt64 nsOggReader::FindEndTime(PRInt64 aEndOffset)
 {
   MonitorAutoEnter mon(mMonitor);
-  NS_ASSERTION(mDecoder->OnStateMachineThread(), "Should be on state machine thread.");
-
+  NS_ASSERTION(mDecoder->OnStateMachineThread(),
+               "Should be on state machine thread.");
+  PRInt64 endTime = FindEndTime(aEndOffset, PR_FALSE, &mOggState);
+  // Reset read head to start of media data.
+  NS_ASSERTION(mDataOffset > 0,
+               "Should have offset of first non-header page");
   nsMediaStream* stream = mDecoder->GetCurrentStream();
-  ogg_sync_reset(&mOggState);
+  NS_ENSURE_TRUE(stream != nsnull, -1);
+  nsresult res = stream->Seek(nsISeekableStream::NS_SEEK_SET, mDataOffset);
+  NS_ENSURE_SUCCESS(res, -1);
+  return endTime;
+}
 
-  stream->Seek(nsISeekableStream::NS_SEEK_SET, aEndOffset);
+PRInt64 nsOggReader::FindEndTime(PRInt64 aEndOffset,
+                                 PRBool aCachedDataOnly,
+                                 ogg_sync_state* aState)
+{
+  nsMediaStream* stream = mDecoder->GetCurrentStream();
+  ogg_sync_reset(aState);
 
   // We need to find the last page which ends before aEndOffset that
   // has a granulepos that we can convert to a timestamp. We do this by
   // backing off from aEndOffset until we encounter a page on which we can
   // interpret the granulepos. If while backing off we encounter a page which
   // we've previously encountered before, we'll either backoff again if we
   // haven't found an end time yet, or return the last end time found.
   const int step = 5000;
-  PRInt64 offset = aEndOffset;
+  PRInt64 readStartOffset = aEndOffset;
+  PRInt64 readHead = aEndOffset;
   PRInt64 endTime = -1;
   PRUint32 checksumAfterSeek = 0;
   PRUint32 prevChecksumAfterSeek = 0;
   PRBool mustBackOff = PR_FALSE;
   while (PR_TRUE) {
-    {
-      MonitorAutoExit exitReaderMon(mMonitor);
-      MonitorAutoEnter decoderMon(mDecoder->GetMonitor());
-      if (mDecoder->GetDecodeState() == nsBuiltinDecoderStateMachine::DECODER_STATE_SHUTDOWN) {
-        return -1;
-      }
-    }
     ogg_page page;    
-    int ret = ogg_sync_pageseek(&mOggState, &page);
+    int ret = ogg_sync_pageseek(aState, &page);
     if (ret == 0) {
       // We need more data if we've not encountered a page we've seen before,
       // or we've read to the end of file.
-      if (mustBackOff || stream->Tell() == aEndOffset) {
+      if (mustBackOff || readHead == aEndOffset) {
         if (endTime != -1) {
           // We have encountered a page before, or we're at the end of file.
           break;
         }
         mustBackOff = PR_FALSE;
         prevChecksumAfterSeek = checksumAfterSeek;
         checksumAfterSeek = 0;
-        ogg_sync_reset(&mOggState);
-        offset = NS_MAX(static_cast<PRInt64>(0), offset - step);
-        stream->Seek(nsISeekableStream::NS_SEEK_SET, offset);
+        ogg_sync_reset(aState);
+        readStartOffset = NS_MAX(static_cast<PRInt64>(0), readStartOffset - step);
+        readHead = readStartOffset;
       }
-      NS_ASSERTION(stream->Tell() < aEndOffset,
-                   "Stream pos must be before range end");
 
       PRInt64 limit = NS_MIN(static_cast<PRInt64>(PR_UINT32_MAX),
-                             aEndOffset - stream->Tell());
+                             aEndOffset - readHead);
       limit = NS_MAX(static_cast<PRInt64>(0), limit);
       limit = NS_MIN(limit, static_cast<PRInt64>(step));
       PRUint32 bytesToRead = static_cast<PRUint32>(limit);
       PRUint32 bytesRead = 0;
-      char* buffer = ogg_sync_buffer(&mOggState,
-                                     bytesToRead);
+      char* buffer = ogg_sync_buffer(aState, bytesToRead);
       NS_ASSERTION(buffer, "Must have buffer");
-      stream->Read(buffer, bytesToRead, &bytesRead);
+      nsresult res;
+      if (aCachedDataOnly) {
+        res = stream->ReadFromCache(buffer, readHead, bytesToRead);
+        NS_ENSURE_SUCCESS(res,res);
+        bytesRead = bytesToRead;
+      } else {
+        NS_ASSERTION(readHead < aEndOffset,
+                     "Stream pos must be before range end");
+        res = stream->Seek(nsISeekableStream::NS_SEEK_SET, readHead);
+        NS_ENSURE_SUCCESS(res,res);
+        res = stream->Read(buffer, bytesToRead, &bytesRead);
+        NS_ENSURE_SUCCESS(res,res);
+      }
+      readHead += bytesRead;
 
       // Update the synchronisation layer with the number
       // of bytes written to the buffer
-      ret = ogg_sync_wrote(&mOggState, bytesRead);
+      ret = ogg_sync_wrote(aState, bytesRead);
       if (ret != 0) {
         endTime = -1;
         break;
       }
 
       continue;
     }
 
@@ -912,38 +931,36 @@ PRInt64 nsOggReader::FindEndTime(PRInt64
     }
 
     PRInt64 t = codecState->Time(granulepos);
     if (t != -1) {
       endTime = t;
     }
   }
 
-  ogg_sync_reset(&mOggState);
-
-  NS_ASSERTION(mDataOffset > 0,
-               "Should have offset of first non-header page");
-  stream->Seek(nsISeekableStream::NS_SEEK_SET, mDataOffset);
+  ogg_sync_reset(aState);
 
   return endTime;
 }
 
 nsresult nsOggReader::Seek(PRInt64 aTarget, PRInt64 aStartTime, PRInt64 aEndTime)
 {
   MonitorAutoEnter mon(mMonitor);
+  nsresult res;
   NS_ASSERTION(mDecoder->OnStateMachineThread(),
                "Should be on state machine thread.");
   LOG(PR_LOG_DEBUG, ("%p About to seek to %lldms", mDecoder, aTarget));
   nsMediaStream* stream = mDecoder->GetCurrentStream();
 
   if (NS_FAILED(ResetDecode())) {
     return NS_ERROR_FAILURE;
   }
   if (aTarget == aStartTime) {
-    stream->Seek(nsISeekableStream::NS_SEEK_SET, mDataOffset);
+    res = stream->Seek(nsISeekableStream::NS_SEEK_SET, mDataOffset);
+    NS_ENSURE_SUCCESS(res, res);
     mPageOffset = mDataOffset;
     NS_ASSERTION(aStartTime != -1, "mStartTime should be known");
     {
       MonitorAutoExit exitReaderMon(mMonitor);
       MonitorAutoEnter decoderMon(mDecoder->GetMonitor());
       mDecoder->UpdatePlaybackPosition(aStartTime);
     }
   } else {
@@ -955,17 +972,17 @@ nsresult nsOggReader::Seek(PRInt64 aTarg
       stream->Unpin();
       return NS_ERROR_FAILURE;
     }
 
     // Try to seek in the cached data ranges first, before falling back to
     // seeking over the network. This makes seeking in buffered ranges almost
     // instantaneous.
     ByteRange r = GetSeekRange(ranges, aTarget, aStartTime, aEndTime, PR_TRUE);
-    nsresult res = NS_ERROR_FAILURE;
+    res = NS_ERROR_FAILURE;
     if (!r.IsNull()) {
       // The frame should be in this buffered range. Seek exactly there.
       res = SeekBisection(aTarget, r, 0);
 
       if (NS_SUCCEEDED(res) && HasVideo()) {
         // We have an active Theora bitstream. Decode the next Theora frame, and
         // extract its keyframe's time.
         PRBool eof;
@@ -1115,49 +1132,59 @@ nsresult nsOggReader::Seek(PRInt64 aTarg
 enum PageSyncResult {
   PAGE_SYNC_ERROR = 1,
   PAGE_SYNC_END_OF_RANGE= 2,
   PAGE_SYNC_OK = 3
 };
 
 // Reads a page from the media stream.
 static PageSyncResult
-PageSync(ogg_sync_state* aState,
-         nsMediaStream* aStream,
+PageSync(nsMediaStream* aStream,
+         ogg_sync_state* aState,
+         PRBool aCachedDataOnly,
+         PRInt64 aOffset,
          PRInt64 aEndOffset,
          ogg_page* aPage,
          int& aSkippedBytes)
 {
   aSkippedBytes = 0;
   // Sync to the next page.
   int ret = 0;
   PRUint32 bytesRead = 0;
+  PRInt64 readHead = aOffset;
   while (ret <= 0) {
     ret = ogg_sync_pageseek(aState, aPage);
     if (ret == 0) {
       char* buffer = ogg_sync_buffer(aState, PAGE_STEP);
       NS_ASSERTION(buffer, "Must have a buffer");
 
       // Read from the file into the buffer
       PRInt64 bytesToRead = NS_MIN(static_cast<PRInt64>(PAGE_STEP),
-                                   aEndOffset - aStream->Tell());
+                                   aEndOffset - readHead);
       if (bytesToRead <= 0) {
         return PAGE_SYNC_END_OF_RANGE;
       }
-      nsresult rv = aStream->Read(buffer,
-                                  static_cast<PRUint32>(bytesToRead),
-                                  &bytesRead);
-      if (NS_FAILED(rv)) {
-        return PAGE_SYNC_ERROR;
+      nsresult rv = NS_OK;
+      if (aCachedDataOnly) {
+        rv = aStream->ReadFromCache(buffer, readHead, bytesToRead);
+        NS_ENSURE_SUCCESS(rv,PAGE_SYNC_ERROR);
+        bytesRead = bytesToRead;
+      } else {
+        rv = aStream->Seek(nsISeekableStream::NS_SEEK_SET, readHead);
+        NS_ENSURE_SUCCESS(rv,PAGE_SYNC_ERROR);
+        rv = aStream->Read(buffer,
+                           static_cast<PRUint32>(bytesToRead),
+                           &bytesRead);
+        NS_ENSURE_SUCCESS(rv,PAGE_SYNC_ERROR);
       }
-
       if (bytesRead == 0 && NS_SUCCEEDED(rv)) {
         // End of file.
         return PAGE_SYNC_END_OF_RANGE;
       }
+      readHead += bytesRead;
 
       // Update the synchronisation layer with the number
       // of bytes written to the buffer
       ret = ogg_sync_wrote(aState, bytesRead);
       NS_ENSURE_TRUE(ret == 0, PAGE_SYNC_ERROR);    
       continue;
     }
 
@@ -1173,23 +1200,25 @@ PageSync(ogg_sync_state* aState,
 }
 
 nsresult nsOggReader::SeekBisection(PRInt64 aTarget,
                                     const ByteRange& aRange,
                                     PRUint32 aFuzz)
 {
   NS_ASSERTION(mDecoder->OnStateMachineThread(),
                "Should be on state machine thread.");
+  nsresult res;
   nsMediaStream* stream = mDecoder->GetCurrentStream();
 
   if (aTarget == aRange.mTimeStart) {
     if (NS_FAILED(ResetDecode())) {
       return NS_ERROR_FAILURE;
     }
-    stream->Seek(nsISeekableStream::NS_SEEK_SET, mDataOffset);
+    res = stream->Seek(nsISeekableStream::NS_SEEK_SET, mDataOffset);
+    NS_ENSURE_SUCCESS(res, res);
     mPageOffset = mDataOffset;
     return NS_OK;
   }
 
   // Bisection search, find start offset of last page with end time less than
   // the seek target.
   ogg_int64_t startOffset = aRange.mOffsetStart;
   ogg_int64_t startTime = aRange.mTimeStart;
@@ -1250,24 +1279,24 @@ nsresult nsOggReader::SeekBisection(PRIn
       previousGuess = guess;
 
       SEEK_LOG(PR_LOG_DEBUG, ("Seek loop offset_start=%lld start_end=%lld "
                               "offset_guess=%lld offset_end=%lld interval=%lld "
                               "target=%lf time_start=%lld time_end=%lld",
                               startOffset, (startOffset+startLength), guess,
                               endOffset, interval, target, startTime, endTime));
       hops++;
-      stream->Seek(nsISeekableStream::NS_SEEK_SET, guess);
     
-      // We've seeked into the media somewhere. Locate the next page, and then
-      // figure out the granule time of the audio and video bitstreams there.
-      // We can then make a bisection decision based on our location in the media.
-      
-      PageSyncResult res = PageSync(&mOggState,
-                                    stream,
+      // Locate the next page after our seek guess, and then figure out the
+      // granule time of the audio and video bitstreams there. We can then
+      // make a bisection decision based on our location in the media.
+      PageSyncResult res = PageSync(stream,
+                                    &mOggState,
+                                    PR_FALSE,
+                                    guess,
                                     endOffset,
                                     &page,
                                     skippedBytes);
       if (res == PAGE_SYNC_ERROR) {
         return NS_ERROR_FAILURE;
       }
 
       // We've located a page of length |ret| at |guess + skippedBytes|.
@@ -1342,28 +1371,30 @@ nsresult nsOggReader::SeekBisection(PRIn
       break;
     }
 
     if (interval == 0) {
       // Seek termination condition; we've found the page boundary of the
       // last page before the target, and the first page after the target.
       SEEK_LOG(PR_LOG_DEBUG, ("Seek loop (interval == 0) break"));
       NS_ASSERTION(startTime < aTarget, "Start time must always be less than target");
-      stream->Seek(nsISeekableStream::NS_SEEK_SET, startOffset);
+      res = stream->Seek(nsISeekableStream::NS_SEEK_SET, startOffset);
+      NS_ENSURE_SUCCESS(res, res);
       mPageOffset = startOffset;
       if (NS_FAILED(ResetDecode())) {
         return NS_ERROR_FAILURE;
       }
       break;
     }
 
     SEEK_LOG(PR_LOG_DEBUG, ("Time at offset %lld is %lldms", guess, granuleTime));
     if (granuleTime < seekTarget && granuleTime > seekLowerBound) {
       // We're within the fuzzy region in which we want to terminate the search.
-      stream->Seek(nsISeekableStream::NS_SEEK_SET, oldPageOffset);
+      res = stream->Seek(nsISeekableStream::NS_SEEK_SET, oldPageOffset);
+      NS_ENSURE_SUCCESS(res,res);
       mPageOffset = oldPageOffset;
       if (NS_FAILED(ResetDecode())) {
         return NS_ERROR_FAILURE;
       }
       break;
     }
 
     if (granuleTime >= seekTarget) {
@@ -1382,12 +1413,95 @@ nsresult nsOggReader::SeekBisection(PRIn
     NS_ASSERTION(endTime >= seekTarget, "End must be after seek target");
   }
 
   SEEK_LOG(PR_LOG_DEBUG, ("Seek complete in %d bisections.", hops));
 
   return NS_OK;
 }
 
-nsresult nsOggReader::GetBuffered(nsHTMLTimeRanges* aBuffered)
+nsresult nsOggReader::GetBuffered(nsHTMLTimeRanges* aBuffered, PRInt64 aStartTime)
 {
+  NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
+  nsMediaStream* stream = mDecoder->GetCurrentStream();
+
+  // Traverse across the buffered byte ranges, determining the time ranges
+  // they contain. nsMediaStream::GetNextCachedData(offset) returns -1 when
+  // offset is after the end of the media stream, or there's no more cached
+  // data after the offset. This loop will run until we've checked every
+  // buffered range in the media, in increasing order of offset.
+  ogg_sync_state state;
+  ogg_sync_init(&state);
+  PRInt64 startOffset = stream->GetNextCachedData(mDataOffset);
+  while (startOffset >= 0) {
+    PRInt64 endOffset = stream->GetCachedDataEnd(startOffset);
+    NS_ASSERTION(startOffset < endOffset, "Buffered range must end after its start");
+    // Bytes [startOffset..endOffset] are cached.
+
+    // Find the start time of the range.
+    PRInt64 startTime = -1;
+    if (startOffset == mDataOffset) {
+      // Because the granulepos time is actually the end time of the page,
+      // we special-case (startOffset == mDataOffset) so that the first
+      // buffered range always appears to be buffered from [t=0...] rather
+      // than from the end-time of the first page.
+      startTime = aStartTime;
+    }
+    // Read pages until we find one with a granulepos which we can convert
+    // into a timestamp to use as the time of the start of the buffered range.
+    ogg_sync_reset(&state);
+    while (startTime == -1) {
+      ogg_page page;
+      PRInt32 discard;
+      PageSyncResult res = PageSync(stream,
+                                    &state,
+                                    PR_TRUE,
+                                    startOffset,
+                                    endOffset,
+                                    &page,
+                                    discard);
+      if (res == PAGE_SYNC_ERROR) {
+        // If we don't clear the sync state before exit we'll leak.
+        ogg_sync_clear(&state);
+        return NS_ERROR_FAILURE;
+      } else if (res == PAGE_SYNC_END_OF_RANGE) {
+        // Hit the end of range without reading a page, give up trying to
+        // find a start time for this buffered range, skip onto the next one.
+        break;
+      }
+
+      PRInt64 granulepos = ogg_page_granulepos(&page);
+      if (granulepos == -1) {
+        // Page doesn't have an end time, advance to the next page
+        // until we find one.
+        startOffset += page.header_len + page.body_len;
+        continue;
+      }
+
+      PRUint32 serial = ogg_page_serialno(&page);
+      nsOggCodecState* codecState = nsnull;
+      mCodecStates.Get(serial, &codecState);
+      if (codecState && codecState->mActive) {
+        startTime = codecState->Time(granulepos) - aStartTime;
+        NS_ASSERTION(startTime > 0, "Must have positive start time");
+      }
+    }
+
+    if (startTime != -1) {
+      // We were able to find a start time for that range, see if we can
+      // find an end time.
+      PRInt64 endTime = FindEndTime(endOffset, PR_TRUE, &state);
+      if (endTime != -1) {
+        endTime -= aStartTime;
+        aBuffered->Add(static_cast<float>(startTime) / 1000.0f,
+                       static_cast<float>(endTime) / 1000.0f);
+      }
+    }
+    startOffset = stream->GetNextCachedData(endOffset);
+    NS_ASSERTION(startOffset == -1 || startOffset > endOffset,
+      "Must have advanced to start of next range, or hit end of stream");
+  }
+
+  // If we don't clear the sync state before exit we'll leak.
+  ogg_sync_clear(&state);
+
   return NS_OK;
 }
--- a/content/media/ogg/nsOggReader.h
+++ b/content/media/ogg/nsOggReader.h
@@ -58,39 +58,53 @@ public:
   virtual nsresult ResetDecode();
   virtual PRBool DecodeAudioData();
 
   // If the Theora granulepos has not been captured, it may read several packets
   // until one with a granulepos has been captured, to ensure that all packets
   // read have valid time info.  
   virtual PRBool DecodeVideoFrame(PRBool &aKeyframeSkip,
                                   PRInt64 aTimeThreshold);
+
   virtual VideoData* FindStartTime(PRInt64 aOffset,
                                    PRInt64& aOutStartTime);
+
+  // Get the end time of aEndOffset. This is the playback position we'd reach
+  // after playback finished at aEndOffset.
   virtual PRInt64 FindEndTime(PRInt64 aEndOffset);
 
   virtual PRBool HasAudio()
   {
     mozilla::MonitorAutoEnter mon(mMonitor);
     return mVorbisState != 0 && mVorbisState->mActive;
   }
 
   virtual PRBool HasVideo()
   {
     mozilla::MonitorAutoEnter mon(mMonitor);
     return mTheoraState != 0 && mTheoraState->mActive;
   }
 
   virtual nsresult ReadMetadata();
   virtual nsresult Seek(PRInt64 aTime, PRInt64 aStartTime, PRInt64 aEndTime);
-
-  // This is called on the main thread, and it must not block.
-  virtual nsresult GetBuffered(nsHTMLTimeRanges* aBuffered);
+  virtual nsresult GetBuffered(nsHTMLTimeRanges* aBuffered, PRInt64 aStartTime);
 
 private:
+
+  // Get the end time of aEndOffset. This is the playback position we'd reach
+  // after playback finished at aEndOffset. If PRBool aCachedDataOnly is
+  // PR_TRUE, then we'll only read from data which is cached in the media cached,
+  // otherwise we'll do regular blocking reads from the media stream.
+  // If PRBool aCachedDataOnly is PR_TRUE, and aState is not mOggState, this can
+  // safely be called on the main thread, otherwise it must be called on the
+  // state machine thread.
+  PRInt64 FindEndTime(PRInt64 aEndOffset,
+                      PRBool aCachedDataOnly,
+                      ogg_sync_state* aState);
+
   // Decodes one packet of Vorbis data, storing the resulting chunks of
   // PCM samples in aChunks.
   nsresult DecodeVorbis(nsTArray<SoundData*>& aChunks,
                         ogg_packet* aPacket);
 
   // May return NS_ERROR_OUT_OF_MEMORY.
   nsresult DecodeTheora(nsTArray<VideoData*>& aFrames,
                         ogg_packet* aPacket);