Bug 449307 - Implement duration support for ogg backend - s+sr=roc
authorChris Double <chris.double@double.co.nz>
Mon, 10 Nov 2008 14:38:02 +1300
changeset 21542 f1d71b8ac3fed48c84b0cbef8a7fb96d879a8d66
parent 21541 fb44ae3d118255a923fa33631c09f77bc19792b7
child 21543 1e69ed317645c7b21b0ebc154b70df7f3404df03
push id3566
push usercdouble@mozilla.com
push dateMon, 10 Nov 2008 01:38:15 +0000
treeherdermozilla-central@f1d71b8ac3fe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs449307
milestone1.9.1b2pre
Bug 449307 - Implement duration support for ogg backend - s+sr=roc
content/media/video/public/nsChannelToPipeListener.h
content/media/video/public/nsMediaDecoder.h
content/media/video/public/nsOggDecoder.h
content/media/video/src/nsChannelToPipeListener.cpp
content/media/video/src/nsMediaStream.cpp
content/media/video/src/nsOggDecoder.cpp
content/media/video/test/Makefile.in
content/media/video/test/test_duration1.html
--- a/content/media/video/public/nsChannelToPipeListener.h
+++ b/content/media/video/public/nsChannelToPipeListener.h
@@ -62,17 +62,19 @@ class nsChannelToPipeListener : public n
 
   // IRequestObserver
   NS_DECL_NSIREQUESTOBSERVER
 
   // IStreamListener
   NS_DECL_NSISTREAMLISTENER
 
   public:
-  nsChannelToPipeListener(nsMediaDecoder* aDecoder);
+  // If aSeeking is PR_TRUE then this listener was created as part of a
+  // seek request and is expecting a byte range partial result.
+  nsChannelToPipeListener(nsMediaDecoder* aDecoder, PRBool aSeeking = PR_FALSE);
   nsresult Init();
   nsresult GetInputStream(nsIInputStream** aStream);
   void Stop();
   void Cancel();
 
   // Return the download rate in bytes per second. Returns 
   // less than zero if the download has complated.
   double BytesPerSecond() const;
@@ -90,11 +92,14 @@ private:
   PRIntervalTime mIntervalStart;
 
   // Interval when last downloaded bytes occurred. Used in computer
   // bytes per second download rate.
   PRIntervalTime mIntervalEnd;
 
   // Total bytes transferred so far
   PRInt64 mTotalBytes;
+
+  // PR_TRUE if this listener is expecting a byte range request result
+  PRPackedBool mSeeking;
 };
 
 #endif
--- a/content/media/video/public/nsMediaDecoder.h
+++ b/content/media/video/public/nsMediaDecoder.h
@@ -140,16 +140,22 @@ class nsMediaDecoder : public nsIObserve
 
   // Return the size of the video file in bytes. Return 0 if the
   // size is unknown or the stream is infinite.
   virtual PRInt64 GetTotalBytes() = 0;
 
   // Set the size of the video file in bytes.
   virtual void SetTotalBytes(PRInt64 aBytes) = 0;
 
+  // Set a flag indicating whether seeking is supported
+  virtual void SetSeekable(PRBool aSeekable) = 0;
+
+  // Return PR_TRUE if seeking is supported.
+  virtual PRBool GetSeekable() = 0;
+
   // Called when the HTML DOM element is bound.
   virtual void ElementAvailable(nsHTMLMediaElement* anElement);
 
   // Called when the HTML DOM element is unbound.
   virtual void ElementUnavailable();
 
   // Invalidate the frame.
   virtual void Invalidate();
--- a/content/media/video/public/nsOggDecoder.h
+++ b/content/media/video/public/nsOggDecoder.h
@@ -337,16 +337,22 @@ class nsOggDecoder : public nsMediaDecod
 
   // Call from any thread safely. Return PR_TRUE if we are currently
   // seeking in the media resource.
   virtual PRBool IsSeeking() const;
 
   // Get the size of the media file in bytes. Called on the main thread only.
   virtual void SetTotalBytes(PRInt64 aBytes);
 
+  // Set a flag indicating whether seeking is supported
+  virtual void SetSeekable(PRBool aSeekable);
+
+  // Return PR_TRUE if seeking is supported.
+  virtual PRBool GetSeekable();
+
 protected:
   // Change to a new play state. This updates the mState variable and
   // notifies any thread blocking on this objects monitor of the
   // change. Can be called on any thread.
   void ChangeState(PlayState aState);
 
   // Returns the monitor for other threads to synchronise access to
   // state
@@ -445,19 +451,28 @@ private:
   // started this is reset to negative.
   float mRequestedSeekTime;
 
   // Size of the media file in bytes. Set on the first non-byte range
   // HTTP request from nsChannelToPipe Listener. Accessed on the
   // main thread only.
   PRInt64 mContentLength;
 
+  // Duration of the media resource. Set to -1 if unknown.
+  // Set when the Ogg metadata is loaded. Accessed on the main thread
+  // only.
+  PRInt64 mDuration;
+
   // True if we are registered with the observer service for shutdown.
   PRPackedBool mNotifyOnShutdown;
 
+  // True if the media resource is seekable (server supports byte range
+  // requests).
+  PRPackedBool mSeekable;
+
   /******
    * The following member variables can be accessed from any thread.
    ******/
 
   // The state machine object for handling the decoding via
   // oggplay. It is safe to call methods of this object from other
   // threads. Its internal data is synchronised on a monitor. The
   // lifetime of this object is after mPlayState is LOADING and before
--- a/content/media/video/src/nsChannelToPipeListener.cpp
+++ b/content/media/video/src/nsChannelToPipeListener.cpp
@@ -37,21 +37,27 @@
  * ***** END LICENSE BLOCK ***** */
 #include "nsAString.h"
 #include "nsNetUtil.h"
 #include "nsMediaDecoder.h"
 #include "nsIScriptSecurityManager.h"
 #include "nsChannelToPipeListener.h"
 #include "nsICachingChannel.h"
 
-nsChannelToPipeListener::nsChannelToPipeListener(nsMediaDecoder* aDecoder) :
+#define HTTP_OK_CODE 200
+#define HTTP_PARTIAL_RESPONSE_CODE 206
+
+nsChannelToPipeListener::nsChannelToPipeListener(
+    nsMediaDecoder* aDecoder,
+    PRBool aSeeking) :
   mDecoder(aDecoder),
   mIntervalStart(0),
   mIntervalEnd(0),
-  mTotalBytes(0)
+  mTotalBytes(0),
+  mSeeking(aSeeking)
 {
 }
 
 nsresult nsChannelToPipeListener::Init() 
 {
   nsresult rv = NS_NewPipe(getter_AddRefs(mInput), 
                            getter_AddRefs(mOutput),
                            0, 
@@ -93,20 +99,35 @@ nsresult nsChannelToPipeListener::OnStar
   mIntervalStart = PR_IntervalNow();
   mIntervalEnd = mIntervalStart;
   mTotalBytes = 0;
   mDecoder->UpdateBytesDownloaded(mTotalBytes);
   nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(aRequest);
   if (hc) {
     PRUint32 responseStatus = 0; 
     hc->GetResponseStatus(&responseStatus);
-    if (responseStatus == 200) {
+    if (mSeeking && responseStatus == HTTP_OK_CODE) {
+      // If we get an OK response but we were seeking,
+      // and therefore expecting a partial response of
+      // HTTP_PARTIAL_RESPONSE_CODE, tell the decoder
+      // we don't support seeking.
+      mDecoder->SetSeekable(PR_FALSE);
+    }
+    else if (!mSeeking && 
+             (responseStatus == HTTP_OK_CODE ||
+              responseStatus == HTTP_PARTIAL_RESPONSE_CODE)) {
+      // We weren't seeking and got a valid response status,
+      // set the length of the content.
       PRInt32 cl = 0;
       hc->GetContentLength(&cl);
       mDecoder->SetTotalBytes(cl);
+
+      // If we get an HTTP_OK_CODE response to our byte range
+      // request, then we don't support seeking.
+      mDecoder->SetSeekable(responseStatus == HTTP_PARTIAL_RESPONSE_CODE);
     }
   }
 
   nsCOMPtr<nsICachingChannel> cc = do_QueryInterface(aRequest);
   if (cc) {
     PRBool fromCache = PR_FALSE;
     nsresult rv = cc->IsFromCache(&fromCache);
 
--- a/content/media/video/src/nsMediaStream.cpp
+++ b/content/media/video/src/nsMediaStream.cpp
@@ -433,16 +433,26 @@ nsresult nsHttpStreamStrategy::Open(nsIS
 
   nsresult rv = mListener->Init();
   NS_ENSURE_SUCCESS(rv, rv);
   
   if (aStreamListener) {
     *aStreamListener = mListener;
     NS_ADDREF(*aStreamListener);
   } else {
+    // Use a byte range request from the start of the resource.
+    // This enables us to detect if the stream supports byte range
+    // requests, and therefore seeking, early.
+    nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(mChannel);
+    if (hc) {
+      hc->SetRequestHeader(NS_LITERAL_CSTRING("Range"),
+          NS_LITERAL_CSTRING("bytes=0-"),
+          PR_FALSE);
+    }
+ 
     rv = mChannel->AsyncOpen(mListener, nsnull);
     NS_ENSURE_SUCCESS(rv, rv);
   }
   
   rv = mListener->GetInputStream(getter_AddRefs(mPipeInput));
   NS_ENSURE_SUCCESS(rv, rv);
 
   mPosition = 0;
@@ -533,17 +543,17 @@ public:
     nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(mChannel);
     if (hc) {
       nsCAutoString rangeString("bytes=");
       rangeString.AppendInt(mOffset);
       rangeString.Append("-");
       hc->SetRequestHeader(NS_LITERAL_CSTRING("Range"), rangeString, PR_FALSE);
     }
 
-    mListener = new nsChannelToPipeListener(mDecoder);
+    mListener = new nsChannelToPipeListener(mDecoder, PR_TRUE);
     NS_ENSURE_TRUE(mListener, NS_ERROR_OUT_OF_MEMORY);
 
     mResult = mListener->Init();
     NS_ENSURE_SUCCESS(mResult, mResult);
 
     mResult = mChannel->AsyncOpen(mListener, nsnull);
     NS_ENSURE_SUCCESS(mResult, mResult);
 
--- a/content/media/video/src/nsOggDecoder.cpp
+++ b/content/media/video/src/nsOggDecoder.cpp
@@ -30,16 +30,17 @@
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
+#include <limits>
 #include "prlog.h"
 #include "prmem.h"
 #include "nsIFrame.h"
 #include "nsIDocument.h"
 #include "nsThreadUtils.h"
 #include "nsIDOMHTMLMediaElement.h"
 #include "nsNetUtil.h"
 #include "nsAudioStream.h"
@@ -262,16 +263,28 @@ public:
   // be locked when calling this method. Returns PR_FALSE if unable to
   // write to the audio device without blocking.
   PRBool PlayAudio(FrameData* aFrame);
 
   // Called from the main thread to get the current frame time. The decoder
   // monitor must be obtained before calling this.
   float GetCurrentTime();
 
+  // Called from the main thread to get the duration. The decoder monitor
+  // must be obtained before calling this. It is in units of milliseconds.
+  PRInt64 GetDuration();
+
+  // Called from the main thread to set the content length of the media
+  // resource. The decoder monitor must be obtained before calling this.
+  void SetContentLength(PRInt64 aLength);
+
+  // Called from the main thread to set whether the media resource can
+  // be seeked. The decoder monitor must be obtained before calling this.
+  void SetSeekable(PRBool aSeekable);
+
   // Get and set the audio volume. The decoder monitor must be
   // obtained before calling this.
   float GetVolume();
   void SetVolume(float aVolume);
 
   // Clear the flag indicating that a playback position change event
   // is currently queued. This is called from the main thread and must
   // be called with the decode monitor held.
@@ -422,16 +435,30 @@ private:
   // time value. Synchronised via decoder monitor.
   float mCurrentFrameTime;
 
   // Volume of playback. 0.0 = muted. 1.0 = full volume. Read/Written
   // from the decode and main threads. Synchronised via decoder
   // monitor.
   float mVolume;
 
+  // Duration of the media resource. It is accessed from the decoder and main
+  // threads. Synchronised via decoder monitor. It is in units of
+  // milliseconds.
+  PRInt64 mDuration;
+
+  // Content Length of the media resource if known. If it is -1 then the
+  // size is unknown. Accessed from the decoder and main threads. Synchronised
+  // via decoder monitor.
+  PRInt64 mContentLength;
+
+  // PR_TRUE if the media resource can be seeked. Accessed from the decoder
+  // and main threads. Synchronised via decoder monitor.
+  PRPackedBool mSeekable;
+
   // PR_TRUE if an event to notify about a change in the playback
   // position has been queued, but not yet run. It is set to PR_FALSE when
   // the event is run. This allows coalescing of these events as they can be
   // produced many times per second. Synchronised via decoder monitor.
   PRPackedBool mPositionChangeQueued;
 };
 
 nsOggDecodeStateMachine::nsOggDecodeStateMachine(nsOggDecoder* aDecoder, nsChannelReader* aReader) :
@@ -450,16 +477,19 @@ nsOggDecodeStateMachine::nsOggDecodeStat
   mReader(aReader),
   mBufferingStart(0),
   mBufferingBytes(0),
   mLastFrameTime(0),
   mState(DECODER_STATE_DECODING_METADATA),
   mSeekTime(0.0),
   mCurrentFrameTime(0.0),
   mVolume(1.0),
+  mDuration(-1),
+  mContentLength(-1),
+  mSeekable(PR_TRUE),
   mPositionChangeQueued(PR_FALSE)
 {
 }
 
 nsOggDecodeStateMachine::~nsOggDecodeStateMachine()
 {
   while (!mDecodedFrames.IsEmpty()) {
     delete mDecodedFrames.Pop();
@@ -768,16 +798,33 @@ void nsOggDecodeStateMachine::SetVolume(
 }
 
 float nsOggDecodeStateMachine::GetCurrentTime()
 {
   //  NS_ASSERTION(PR_InMonitor(mDecoder->GetMonitor()), "GetCurrentTime() called without acquiring decoder monitor");
   return mCurrentFrameTime;
 }
 
+PRInt64 nsOggDecodeStateMachine::GetDuration()
+{
+  //  NS_ASSERTION(PR_InMonitor(mDecoder->GetMonitor()), "GetDuration() called without acquiring decoder monitor");
+  return mDuration;
+}
+
+void nsOggDecodeStateMachine::SetContentLength(PRInt64 aLength)
+{
+  //  NS_ASSERTION(PR_InMonitor(mDecoder->GetMonitor()), "SetContentLength() called without acquiring decoder monitor");
+  mContentLength = aLength;
+}
+
+void nsOggDecodeStateMachine::SetSeekable(PRBool aSeekable)
+{
+   //  NS_ASSERTION(PR_InMonitor(mDecoder->GetMonitor()), "SetSeekable() called without acquiring decoder monitor");
+  mSeekable = aSeekable;
+}
 
 void nsOggDecodeStateMachine::Shutdown()
 {
   // oggplay_prepare_for_close cannot be undone. Once called, the
   // mPlayer object cannot decode any more frames. Once we've entered
   // the shutdown state here there's no going back.
   nsAutoMonitor mon(mDecoder->GetMonitor());
   if (mPlayer) {
@@ -1039,30 +1086,51 @@ void nsOggDecodeStateMachine::LoadOggHea
       }
       else if (mAudioTrack == -1 && oggplay_get_track_type(mPlayer, i) == OGGZ_CONTENT_VORBIS) {
         mAudioTrack = i;
         oggplay_set_offset(mPlayer, i, OGGPLAY_AUDIO_OFFSET);
         oggplay_get_audio_samplerate(mPlayer, i, &mAudioRate);
         oggplay_get_audio_channels(mPlayer, i, &mAudioChannels);
         LOG(PR_LOG_DEBUG, ("samplerate: %d, channels: %d", mAudioRate, mAudioChannels));
       }
-      
+ 
       if (oggplay_set_track_active(mPlayer, i) < 0)  {
         LOG(PR_LOG_ERROR, ("Could not set track %d active", i));
       }
     }
-    
+
     if (mVideoTrack == -1) {
       oggplay_set_callback_num_frames(mPlayer, mAudioTrack, OGGPLAY_FRAMES_PER_CALLBACK);
       mCallbackPeriod = 1.0 / (float(mAudioRate) / OGGPLAY_FRAMES_PER_CALLBACK);
     }
     LOG(PR_LOG_DEBUG, ("Callback Period: %f", mCallbackPeriod));
 
     oggplay_use_buffer(mPlayer, OGGPLAY_BUFFER_SIZE);
 
+    // Get the duration from the Ogg file. We only do this if the
+    // content length of the resource is known as we need to seek
+    // to the end of the file to get the last time field. We also
+    // only do this if the resource is seekable.
+    {
+      nsAutoMonitor mon(mDecoder->GetMonitor());
+      if (mState != DECODER_STATE_SHUTDOWN &&
+          mContentLength >= 0 && 
+          mSeekable) {
+        // Don't hold the monitor during the duration
+        // call as it can issue seek requests
+        // and blocks until these are completed.
+        mon.Exit();
+        PRInt64 d = oggplay_get_duration(mPlayer);
+        mon.Enter();
+        mDuration = d;
+      }
+      if (mState == DECODER_STATE_SHUTDOWN)
+        return;
+    }
+
     // Inform the element that we've loaded the Ogg metadata
     nsCOMPtr<nsIRunnable> metadataLoadedEvent = 
       NS_NEW_RUNNABLE_METHOD(nsOggDecoder, mDecoder, MetadataLoaded); 
     
     NS_DispatchToMainThread(metadataLoadedEvent, NS_DISPATCH_NORMAL);
   }
 }
 
@@ -1092,30 +1160,32 @@ void nsOggDecoder::SetVolume(float volum
 
   if (mDecodeStateMachine) {
     mDecodeStateMachine->SetVolume(volume);
   }
 }
 
 float nsOggDecoder::GetDuration()
 {
-  // Currently not implemented. Video Spec says to return
-  // NaN if unknown.
-  // TODO: return NaN
-  return 0.0;
+  if (mDuration >= 0) {
+     return static_cast<float>(mDuration) / 1000.0;
+  }
+
+  return std::numeric_limits<float>::quiet_NaN();
 }
 
 nsOggDecoder::nsOggDecoder() :
   nsMediaDecoder(),
   mBytesDownloaded(0),
   mCurrentTime(0.0),
   mInitialVolume(0.0),
   mRequestedSeekTime(-1.0),
-  mContentLength(0),
+  mContentLength(-1),
   mNotifyOnShutdown(PR_FALSE),
+  mSeekable(PR_TRUE),
   mReader(0),
   mMonitor(0),
   mPlayState(PLAY_STATE_PAUSED),
   mNextState(PLAY_STATE_PAUSED)
 {
   MOZ_COUNT_CTOR(nsOggDecoder);
 }
 
@@ -1196,16 +1266,21 @@ nsresult nsOggDecoder::Load(nsIURI* aURI
 
   nsresult rv = mReader->Init(this, mURI, aChannel, aStreamListener);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = NS_NewThread(getter_AddRefs(mDecodeThread));
   NS_ENSURE_SUCCESS(rv, rv);
 
   mDecodeStateMachine = new nsOggDecodeStateMachine(this, mReader);
+  {
+    nsAutoMonitor mon(mMonitor);
+    mDecodeStateMachine->SetContentLength(mContentLength);
+    mDecodeStateMachine->SetSeekable(mSeekable);
+  }
 
   ChangeState(PLAY_STATE_LOADING);
 
   return mDecodeThread->Dispatch(mDecodeStateMachine, NS_DISPATCH_NORMAL);
 }
 
 nsresult nsOggDecoder::Play()
 {
@@ -1297,16 +1372,21 @@ nsIPrincipal* nsOggDecoder::GetCurrentPr
     return nsnull;
   }
 
   return mReader->GetCurrentPrincipal();
 }
 
 void nsOggDecoder::MetadataLoaded()
 {
+  {
+    nsAutoMonitor mon(mMonitor);
+    mDuration = mDecodeStateMachine ? mDecodeStateMachine->GetDuration() : -1;
+  }
+
   if (mElement) {
     mElement->MetadataLoaded();
   }
 }
 
 void nsOggDecoder::FirstFrameLoaded()
 {
   if (mElement) {
@@ -1375,16 +1455,20 @@ PRUint64 nsOggDecoder::GetBytesLoaded()
 PRInt64 nsOggDecoder::GetTotalBytes()
 {
   return mContentLength;
 }
 
 void nsOggDecoder::SetTotalBytes(PRInt64 aBytes)
 {
   mContentLength = aBytes;
+  if (mDecodeStateMachine) {
+    nsAutoMonitor mon(mMonitor);
+    mDecodeStateMachine->SetContentLength(aBytes);
+  } 
 }
 
 void nsOggDecoder::UpdateBytesDownloaded(PRUint64 aBytes)
 {
   mBytesDownloaded = aBytes;
 }
 
 void nsOggDecoder::BufferingStopped()
@@ -1542,8 +1626,23 @@ void nsOggDecoder::PlaybackPositionChang
   // event runs JavaScript that queries the media size, the
   // frame has reflowed and the size updated beforehand.
   Invalidate();
 
   if (mElement && lastTime != mCurrentTime) {
     mElement->DispatchSimpleEvent(NS_LITERAL_STRING("timeupdate"));
   }
 }
+
+void nsOggDecoder::SetSeekable(PRBool aSeekable)
+{
+  mSeekable = aSeekable;
+  if (mDecodeStateMachine) {
+    nsAutoMonitor mon(mMonitor);
+    mDecodeStateMachine->SetSeekable(aSeekable);
+  }
+}
+
+PRBool nsOggDecoder::GetSeekable()
+{
+  return mSeekable;
+}
+
--- a/content/media/video/test/Makefile.in
+++ b/content/media/video/test/Makefile.in
@@ -43,16 +43,17 @@ relativesrcdir  = content/media/video/te
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES = 	test_autoplay.html \
                 test_bug461281.html \
                 test_constants.html \
                 test_controls.html \
                 test_currentTime.html \
+                test_duration1.html \
                 test_ended1.html \
                 test_ended2.html \
                 test_networkState.html \
                 test_paused.html \
                 test_readyState.html \
                 test_seek1.html \
                 test_seek2.html \
                 test_seek3.html \
new file mode 100644
--- /dev/null
+++ b/content/media/video/test/test_duration1.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Media test: seek test 1</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>
+<video id='v'
+       src='seek.ogg'
+       onloadedmetadata='return startTest();'></video>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+// Test that getting the duration from a file works
+var completed = false;
+var timeout;
+
+function startTest() {
+  if (completed)
+    return false;
+  var v = document.getElementById('v');
+  ok(Math.round(v.duration*1000) == 3833, "Check duration of video: " + v.duration);
+  completed = true;
+  clearTimeout(timeout);
+  SimpleTest.finish();
+  return false;
+}
+
+timeout = setTimeout(function () {
+		       ok(false, "Test timed out");
+		       SimpleTest.finish();
+		     }, 60000);
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>