Bug 495040 - Implement playbackRate and related bits r=kinetik
authorPaul Adenot <paul@paul.cx>
Thu, 22 Nov 2012 11:38:28 +0100
changeset 113986 265233a61624
parent 113985 98a1b656277b
child 113987 fd80fa7611e0
push id18488
push userpaul@paul.cx
push dateThu, 22 Nov 2012 10:38:41 +0000
treeherdermozilla-inbound@a20b32f37e7a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskinetik
bugs495040
milestone20.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 495040 - Implement playbackRate and related bits r=kinetik
content/html/content/public/nsHTMLMediaElement.h
content/html/content/src/nsHTMLMediaElement.cpp
content/media/AudioSampleFormat.h
content/media/AudioStream.cpp
content/media/AudioStream.h
content/media/MediaDecoder.cpp
content/media/MediaDecoder.h
content/media/MediaDecoderStateMachine.cpp
content/media/MediaDecoderStateMachine.h
dom/interfaces/html/nsIDOMHTMLAudioElement.idl
dom/interfaces/html/nsIDOMHTMLMediaElement.idl
dom/interfaces/html/nsIDOMHTMLVideoElement.idl
--- a/content/html/content/public/nsHTMLMediaElement.h
+++ b/content/html/content/public/nsHTMLMediaElement.h
@@ -641,16 +641,21 @@ protected:
    **/
   void GetCurrentSpec(nsCString& aString);
 
   /**
    * Process any media fragment entries in the URI
    */
   void ProcessMediaFragmentURI();
 
+  /**
+   * Mute or unmute the audio, without changing the value that |muted| reports.
+   */
+  void SetMutedInternal(bool aMuted);
+
   // Get the nsHTMLMediaElement object if the decoder is being used from an
   // HTML media element, and null otherwise.
   virtual nsHTMLMediaElement* GetMediaElement() MOZ_FINAL MOZ_OVERRIDE
   {
     return this;
   }
 
   // Return true if decoding should be paused
@@ -784,16 +789,30 @@ protected:
   // fragment time has been set. Read/Write from the main thread only.
   double mFragmentStart;
 
   // Logical end time of the media resource in seconds as obtained
   // from any media fragments. A negative value indicates that no
   // fragment time has been set. Read/Write from the main thread only.
   double mFragmentEnd;
 
+  // The defaultPlaybackRate attribute gives the desired speed at which the
+  // media resource is to play, as a multiple of its intrinsic speed.
+  double mDefaultPlaybackRate;
+
+  // The playbackRate attribute gives the speed at which the media resource
+  // plays, as a multiple of its intrinsic speed. If it is not equal to the
+  // defaultPlaybackRate, then the implication is that the user is using a
+  // feature such as fast forward or slow motion playback.
+  double mPlaybackRate;
+
+  // True if pitch correction is applied when playbackRate is set to a
+  // non-intrinsic value.
+  bool mPreservesPitch;
+
   nsRefPtr<gfxASurface> mPrintSurface;
 
   // Reference to the source element last returned by GetNextSource().
   // This is the child source element which we're trying to load from.
   nsCOMPtr<nsIContent> mSourceLoadCandidate;
 
   // An audio stream for writing audio directly from JS.
   nsAutoPtr<AudioStream> mAudioStream;
--- a/content/html/content/src/nsHTMLMediaElement.cpp
+++ b/content/html/content/src/nsHTMLMediaElement.cpp
@@ -118,16 +118,28 @@ static PRLogModuleInfo* gMediaElementEve
 using namespace mozilla;
 using namespace mozilla::dom;
 using namespace mozilla::layers;
 
 
 // Number of milliseconds between timeupdate events as defined by spec
 #define TIMEUPDATE_MS 250
 
+// These constants are arbitrary
+// Minimum playbackRate for a media
+static const double MIN_PLAYBACKRATE = 0.25;
+// Maximum playbackRate for a media
+static const double MAX_PLAYBACKRATE = 5.0;
+// These are the limits beyonds which SoundTouch does not perform too well and when
+// speech is hard to understand anyway.
+// Threshold above which audio is muted
+static const double THRESHOLD_HIGH_PLAYBACKRATE_AUDIO = 4.0;
+// Threshold under which audio is muted
+static const double THRESHOLD_LOW_PLAYBACKRATE_AUDIO = 0.5;
+
 // Under certain conditions there may be no-one holding references to
 // a media element from script, DOM parent, etc, but the element may still
 // fire meaningful events in the future so we can't destroy it yet:
 // 1) If the element is delaying the load event (or would be, if it were
 // in a document), then events up to loadeddata or error could be fired,
 // so we need to stay alive.
 // 2) If the element is not paused and playback has not ended, then
 // we will (or might) play, sending timeupdate and ended events and possibly
@@ -704,16 +716,17 @@ void nsHTMLMediaElement::QueueSelectReso
 /* void load (); */
 NS_IMETHODIMP nsHTMLMediaElement::Load()
 {
   if (mIsRunningLoadMethod)
     return NS_OK;
   SetPlayedOrSeeked(false);
   mIsRunningLoadMethod = true;
   AbortExistingLoads();
+  SetPlaybackRate(mDefaultPlaybackRate);
   QueueSelectResourceTask();
   mIsRunningLoadMethod = false;
   return NS_OK;
 }
 
 static bool HasSourceChildren(nsIContent *aElement)
 {
   for (nsIContent* child = aElement->GetFirstChild();
@@ -1180,16 +1193,17 @@ nsresult nsHTMLMediaElement::LoadWithCha
 
   ChangeDelayLoadStatus(true);
   rv = InitializeDecoderForChannel(aChannel, aListener);
   if (NS_FAILED(rv)) {
     ChangeDelayLoadStatus(false);
     return rv;
   }
 
+  SetPlaybackRate(mDefaultPlaybackRate);
   DispatchAsyncEvent(NS_LITERAL_STRING("loadstart"));
 
   return NS_OK;
 }
 
 NS_IMETHODIMP nsHTMLMediaElement::MozLoadFrom(nsIDOMHTMLMediaElement* aOther)
 {
   NS_ENSURE_ARG_POINTER(aOther);
@@ -1210,16 +1224,17 @@ NS_IMETHODIMP nsHTMLMediaElement::MozLoa
 
   mLoadingSrc = other->mLoadingSrc;
   nsresult rv = InitializeDecoderAsClone(other->mDecoder);
   if (NS_FAILED(rv)) {
     ChangeDelayLoadStatus(false);
     return rv;
   }
 
+  SetPlaybackRate(mDefaultPlaybackRate);
   DispatchAsyncEvent(NS_LITERAL_STRING("loadstart"));
 
   return NS_OK;
 }
 
 /* readonly attribute unsigned short readyState; */
 NS_IMETHODIMP nsHTMLMediaElement::GetReadyState(uint16_t *aReadyState)
 {
@@ -1527,31 +1542,35 @@ nsHTMLMediaElement::SetMozFrameBufferLen
 /* attribute boolean muted; */
 NS_IMETHODIMP nsHTMLMediaElement::GetMuted(bool *aMuted)
 {
   *aMuted = mMuted;
 
   return NS_OK;
 }
 
-NS_IMETHODIMP nsHTMLMediaElement::SetMuted(bool aMuted)
+void nsHTMLMediaElement::SetMutedInternal(bool aMuted)
 {
-  if (aMuted == mMuted)
-    return NS_OK;
-
-  mMuted = aMuted;
-
-  float effectiveVolume = mMuted ? 0.0f : float(mVolume);
+  float effectiveVolume = aMuted ? 0.0f : float(mVolume);
   if (mDecoder) {
     mDecoder->SetVolume(effectiveVolume);
   } else if (mAudioStream) {
     mAudioStream->SetVolume(effectiveVolume);
   } else if (mSrcStream) {
     GetSrcMediaStream()->SetAudioOutputVolume(this, effectiveVolume);
   }
+}
+
+NS_IMETHODIMP nsHTMLMediaElement::SetMuted(bool aMuted)
+{
+  if (aMuted == mMuted)
+    return NS_OK;
+
+  mMuted = aMuted;
+  SetMutedInternal(aMuted);
 
   DispatchAsyncEvent(NS_LITERAL_STRING("volumechange"));
 
   return NS_OK;
 }
 
 already_AddRefed<nsDOMMediaStream>
 nsHTMLMediaElement::CaptureStreamInternal(bool aFinishWhenEnded)
@@ -1708,16 +1727,19 @@ nsHTMLMediaElement::nsHTMLMediaElement(a
     mVolume(1.0),
     mChannels(0),
     mRate(0),
     mPreloadAction(PRELOAD_UNDEFINED),
     mMediaSize(-1,-1),
     mLastCurrentTime(0.0),
     mFragmentStart(-1.0),
     mFragmentEnd(-1.0),
+    mDefaultPlaybackRate(1.0),
+    mPlaybackRate(1.0),
+    mPreservesPitch(true),
     mCurrentPlayRangeStart(-1.0),
     mAllowAudioData(false),
     mBegun(false),
     mLoadedFirstFrame(false),
     mAutoplaying(true),
     mAutoplayEnabled(true),
     mPaused(true),
     mMuted(false),
@@ -1852,17 +1874,16 @@ NS_IMETHODIMP nsHTMLMediaElement::Play()
   }
 
   if (mCurrentPlayRangeStart == -1.0) {
     GetCurrentTime(&mCurrentPlayRangeStart);
   }
 
   // TODO: If the playback has ended, then the user agent must set
   // seek to the effective start.
-  // TODO: The playback rate must be set to the default playback rate.
   if (mPaused) {
     if (mSrcStream) {
       GetSrcMediaStream()->ChangeExplicitBlockerCount(-1);
     }
     DispatchAsyncEvent(NS_LITERAL_STRING("play"));
     switch (mReadyState) {
     case nsIDOMHTMLMediaElement::HAVE_NOTHING:
       DispatchAsyncEvent(NS_LITERAL_STRING("waiting"));
@@ -1874,16 +1895,18 @@ NS_IMETHODIMP nsHTMLMediaElement::Play()
       break;
     case nsIDOMHTMLMediaElement::HAVE_FUTURE_DATA:
     case nsIDOMHTMLMediaElement::HAVE_ENOUGH_DATA:
       DispatchAsyncEvent(NS_LITERAL_STRING("playing"));
       break;
     }
   }
 
+  SetPlaybackRate(mDefaultPlaybackRate);
+
   mPaused = false;
   mAutoplaying = false;
   // We changed mPaused and mAutoplaying which can affect AddRemoveSelfReference
   // and our preload status.
   AddRemoveSelfReference();
   UpdatePreloadAction();
 
   return NS_OK;
@@ -3710,16 +3733,92 @@ void nsHTMLMediaElement::GetMimeType(nsC
 
 void nsHTMLMediaElement::NotifyAudioAvailableListener()
 {
   if (mDecoder) {
     mDecoder->NotifyAudioAvailableListener();
   }
 }
 
+static double ClampPlaybackRate(double aPlaybackRate)
+{
+  if (aPlaybackRate == 0.0) {
+    return aPlaybackRate;
+  }
+  if (NS_ABS(aPlaybackRate) < MIN_PLAYBACKRATE) {
+    return aPlaybackRate < 0 ? -MIN_PLAYBACKRATE : MIN_PLAYBACKRATE;
+  }
+  if (NS_ABS(aPlaybackRate) > MAX_PLAYBACKRATE) {
+    return aPlaybackRate < 0 ? -MAX_PLAYBACKRATE : MAX_PLAYBACKRATE;
+  }
+  return aPlaybackRate;
+}
+
+/* attribute double defaultPlaybackRate; */
+NS_IMETHODIMP nsHTMLMediaElement::GetDefaultPlaybackRate(double* aDefaultPlaybackRate)
+{
+  *aDefaultPlaybackRate = mDefaultPlaybackRate;
+  return NS_OK;
+}
+
+NS_IMETHODIMP nsHTMLMediaElement::SetDefaultPlaybackRate(double aDefaultPlaybackRate)
+{
+  if (aDefaultPlaybackRate < 0) {
+    return NS_ERROR_NOT_IMPLEMENTED;
+  }
+
+  mDefaultPlaybackRate = ClampPlaybackRate(aDefaultPlaybackRate);
+  DispatchAsyncEvent(NS_LITERAL_STRING("ratechange"));
+  return NS_OK;
+}
+
+/* attribute double playbackRate; */
+NS_IMETHODIMP nsHTMLMediaElement::GetPlaybackRate(double* aPlaybackRate)
+{
+  *aPlaybackRate = mPlaybackRate;
+  return NS_OK;
+}
+
+NS_IMETHODIMP nsHTMLMediaElement::SetPlaybackRate(double aPlaybackRate)
+{
+  if (aPlaybackRate < 0) {
+    return NS_ERROR_NOT_IMPLEMENTED;
+  }
+
+  mPlaybackRate = ClampPlaybackRate(aPlaybackRate);
+
+  if (mPlaybackRate < 0 ||
+      mPlaybackRate > THRESHOLD_HIGH_PLAYBACKRATE_AUDIO ||
+      mPlaybackRate < THRESHOLD_LOW_PLAYBACKRATE_AUDIO) {
+    SetMutedInternal(true);
+  } else {
+    SetMutedInternal(false);
+  }
+
+  if (mDecoder) {
+    mDecoder->SetPlaybackRate(mPlaybackRate);
+  }
+  DispatchAsyncEvent(NS_LITERAL_STRING("ratechange"));
+  return NS_OK;
+}
+
+/* attribute bool mozPreservesPitch; */
+NS_IMETHODIMP nsHTMLMediaElement::GetMozPreservesPitch(bool* aPreservesPitch)
+{
+  *aPreservesPitch = mPreservesPitch;
+  return NS_OK;
+}
+
+NS_IMETHODIMP nsHTMLMediaElement::SetMozPreservesPitch(bool aPreservesPitch)
+{
+  mPreservesPitch = aPreservesPitch;
+  mDecoder->SetPreservesPitch(aPreservesPitch);
+  return NS_OK;
+}
+
 NS_IMETHODIMP
 nsHTMLMediaElement::GetMozAudioChannelType(nsAString& aString)
 {
   switch (mAudioChannelType) {
     case AUDIO_CHANNEL_NORMAL:
       aString.AssignLiteral("normal");
       break;
     case AUDIO_CHANNEL_CONTENT:
--- a/content/media/AudioSampleFormat.h
+++ b/content/media/AudioSampleFormat.h
@@ -125,11 +125,30 @@ ConvertAudioSamplesWithScale(const int16
     }
     return;
   }
   for (int i = 0; i < aCount; ++i) {
     aTo[i] = FloatToAudioSample<int16_t>(AudioSampleToFloat(aFrom[i])*aScale);
   }
 }
 
+// In place audio sample scaling.
+inline void
+ScaleAudioSamples(float* aBuffer, int aCount, float aScale)
+{
+  for (int32_t i = 0; i < aCount; ++i) {
+    aBuffer[i] *= aScale;
+  }
+}
+
+
+inline void
+ScaleAudioSamples(short* aBuffer, int aCount, float aScale)
+{
+  int32_t volume = int32_t(1 << 16) * aScale;
+  for (int32_t i = 0; i < aCount; ++i) {
+    aBuffer[i] = short((int32_t(aBuffer[i]) * volume) >> 16);
+  }
+}
+
 } // namespace mozilla
 
 #endif /* MOZILLA_AUDIOSAMPLEFORMAT_H_ */
--- a/content/media/AudioStream.cpp
+++ b/content/media/AudioStream.cpp
@@ -58,20 +58,23 @@ class nsNativeAudioStream : public Audio
   nsresult Write(const AudioDataValue* aBuf, uint32_t aFrames);
   uint32_t Available();
   void SetVolume(double aVolume);
   void Drain();
   void Pause();
   void Resume();
   int64_t GetPosition();
   int64_t GetPositionInFrames();
+  int64_t GetPositionInFramesInternal();
   bool IsPaused();
   int32_t GetMinWriteSize();
 
  private:
+  int32_t WriteToBackend(const float* aBuffer, uint32_t aFrames);
+  int32_t WriteToBackend(const short* aBuffer, uint32_t aFrames);
 
   double mVolume;
   void* mAudioHandle;
 
   // True if this audio stream is paused.
   bool mPaused;
 
   // True if this stream has encountered an error.
@@ -167,16 +170,23 @@ static sa_stream_type_t ConvertChannelTo
     case dom::AUDIO_CHANNEL_PUBLICNOTIFICATION:
       return SA_STREAM_TYPE_ENFORCED_AUDIBLE;
     default:
       NS_ERROR("The value of AudioChannelType is invalid");
       return SA_STREAM_TYPE_MAX;
   }
 }
 
+AudioStream::AudioStream()
+: mInRate(0),
+  mOutRate(0),
+  mChannels(0),
+  mAudioClock(this)
+{}
+
 void AudioStream::InitLibrary()
 {
 #ifdef PR_LOGGING
   gAudioStreamLog = PR_NewLogModule("AudioStream");
 #endif
   gAudioPrefsLock = new Mutex("AudioStream::gAudioPrefsLock");
   PrefChanged(PREF_VOLUME_SCALE, nullptr);
   Preferences::RegisterCallback(PrefChanged, PREF_VOLUME_SCALE);
@@ -204,33 +214,93 @@ void AudioStream::ShutdownLibrary()
   }
 #endif
 }
 
 AudioStream::~AudioStream()
 {
 }
 
+bool AudioStream::EnsureTimeStretcherInitialized()
+{
+  if (mTimeStretcher)
+    return true;
+  soundtouch::SoundTouch* state = new soundtouch::SoundTouch();
+  if (!state) {
+    return false;
+  }
+  mTimeStretcher.own(state);
+  mTimeStretcher->setSampleRate(mInRate);
+  mTimeStretcher->setChannels(mChannels);
+  mTimeStretcher->setPitch(1.0);
+  return true;
+}
+
+nsresult AudioStream::SetPlaybackRate(double aPlaybackRate)
+{
+  NS_ASSERTION(aPlaybackRate > 0.0,
+               "Can't handle negative or null playbackrate in the AudioStream.");
+  // Avoid instantiating the resampler if we are not changing the playback rate.
+  if (aPlaybackRate == mAudioClock.GetPlaybackRate()) {
+    return NS_OK;
+  }
+  mAudioClock.SetPlaybackRate(aPlaybackRate);
+  mOutRate = mInRate / aPlaybackRate;
+  if (!EnsureTimeStretcherInitialized()) {
+    return NS_ERROR_FAILURE;
+  }
+  if (mAudioClock.GetPreservesPitch()) {
+    mTimeStretcher->setTempo(aPlaybackRate);
+    mTimeStretcher->setRate(1.0f);
+  } else {
+    mTimeStretcher->setTempo(1.0f);
+    mTimeStretcher->setRate(aPlaybackRate);
+  }
+  return NS_OK;
+}
+
+nsresult AudioStream::SetPreservesPitch(bool aPreservesPitch)
+{
+  // Avoid instantiating the timestretcher instance if not needed.
+  if (aPreservesPitch == mAudioClock.GetPreservesPitch()) {
+    return NS_OK;
+  }
+  if (!EnsureTimeStretcherInitialized()) {
+    return NS_ERROR_FAILURE;
+  }
+  if (aPreservesPitch == true) {
+    mTimeStretcher->setTempo(mAudioClock.GetPlaybackRate());
+    mTimeStretcher->setRate(1.0f);
+  } else {
+    mTimeStretcher->setTempo(1.0f);
+    mTimeStretcher->setRate(mAudioClock.GetPlaybackRate());
+  }
+
+  mAudioClock.SetPreservesPitch(aPreservesPitch);
+
+  return NS_OK;
+}
+
 nsNativeAudioStream::nsNativeAudioStream() :
   mVolume(1.0),
   mAudioHandle(0),
   mPaused(false),
   mInError(false)
 {
 }
 
 nsNativeAudioStream::~nsNativeAudioStream()
 {
   Shutdown();
 }
 
 nsresult nsNativeAudioStream::Init(int32_t aNumChannels, int32_t aRate,
                                    const dom::AudioChannelType aAudioChannelType)
 {
-  mRate = aRate;
+  mInRate = mOutRate = aRate;
   mChannels = aNumChannels;
 
   if (sa_stream_create_pcm(reinterpret_cast<sa_stream_t**>(&mAudioHandle),
                            NULL,
                            SA_MODE_WRONLY,
                            SA_PCM_FORMAT_S16_NE,
                            aRate,
                            aNumChannels) != SA_SUCCESS) {
@@ -253,46 +323,80 @@ nsresult nsNativeAudioStream::Init(int32
     sa_stream_destroy(static_cast<sa_stream_t*>(mAudioHandle));
     mAudioHandle = nullptr;
     mInError = true;
     PR_LOG(gAudioStreamLog, PR_LOG_ERROR, ("nsNativeAudioStream: sa_stream_open error"));
     return NS_ERROR_FAILURE;
   }
   mInError = false;
 
+  mAudioClock.Init();
+
   return NS_OK;
 }
 
 void nsNativeAudioStream::Shutdown()
 {
   if (!mAudioHandle)
     return;
 
   sa_stream_destroy(static_cast<sa_stream_t*>(mAudioHandle));
   mAudioHandle = nullptr;
   mInError = true;
 }
 
+int32_t nsNativeAudioStream::WriteToBackend(const AudioDataValue* aBuffer, uint32_t aSamples)
+{
+  double scaledVolume = GetVolumeScale() * mVolume;
+
+  nsAutoArrayPtr<short> outputBuffer(new short[aSamples]);
+  ConvertAudioSamplesWithScale(aBuffer, outputBuffer.get(), aSamples, scaledVolume);
+
+  if (sa_stream_write(static_cast<sa_stream_t*>(mAudioHandle),
+                      outputBuffer,
+                      aSamples * sizeof(short)) != SA_SUCCESS) {
+    return -1;
+  }
+  mAudioClock.UpdateWritePosition(aSamples / mChannels);
+  return aSamples;
+}
+
 nsresult nsNativeAudioStream::Write(const AudioDataValue* aBuf, uint32_t aFrames)
 {
   NS_ASSERTION(!mPaused, "Don't write audio when paused, you'll block");
 
   if (mInError)
     return NS_ERROR_FAILURE;
 
   uint32_t samples = aFrames * mChannels;
-  nsAutoArrayPtr<short> s_data(new short[samples]);
-
-  float scaled_volume = float(GetVolumeScale() * mVolume);
-  ConvertAudioSamplesWithScale(aBuf, s_data.get(), samples, scaled_volume);
+  int32_t written = -1;
 
-  if (sa_stream_write(static_cast<sa_stream_t*>(mAudioHandle),
-                      s_data.get(),
-                      samples * sizeof(short)) != SA_SUCCESS)
-  {
+  if (mInRate != mOutRate) {
+    if (!EnsureTimeStretcherInitialized()) {
+      return NS_ERROR_FAILURE;
+    }
+    mTimeStretcher->putSamples(aBuf, aFrames);
+    uint32_t numFrames = mTimeStretcher->numSamples();
+    uint32_t arraySize = numFrames * mChannels * sizeof(AudioDataValue);
+    nsAutoArrayPtr<AudioDataValue> data(new AudioDataValue[arraySize]);
+    uint32_t framesAvailable = mTimeStretcher->receiveSamples(data, numFrames);
+    NS_ASSERTION(mTimeStretcher->numSamples() == 0,
+                 "We did not get all the data from the SoundTouch pipeline.");
+    // It is possible to have nothing to write: the data are in the processing
+    // pipeline, and will be written to the backend next time.
+    if (framesAvailable) {
+      written = WriteToBackend(data, framesAvailable * mChannels);
+    } else {
+      written = 0;
+    }
+  } else {
+    written = WriteToBackend(aBuf, samples);
+  }
+
+  if (written == -1) {
     PR_LOG(gAudioStreamLog, PR_LOG_ERROR, ("nsNativeAudioStream: sa_stream_write error"));
     mInError = true;
     return NS_ERROR_FAILURE;
   }
   return NS_OK;
 }
 
 uint32_t nsNativeAudioStream::Available()
@@ -321,16 +425,36 @@ void nsNativeAudioStream::SetVolume(doub
   mVolume = aVolume;
 #endif
 }
 
 void nsNativeAudioStream::Drain()
 {
   NS_ASSERTION(!mPaused, "Don't drain audio when paused, it won't finish!");
 
+  // Write all the frames still in the time stretcher pipeline.
+  if (mTimeStretcher) {
+    uint32_t numFrames = mTimeStretcher->numSamples();
+    uint32_t arraySize = numFrames * mChannels * sizeof(AudioDataValue);
+    nsAutoArrayPtr<AudioDataValue> data(new AudioDataValue[arraySize]);
+    uint32_t framesAvailable = mTimeStretcher->receiveSamples(data, numFrames);
+    int32_t written = 0;
+    if (framesAvailable) {
+      written = WriteToBackend(data, framesAvailable * mChannels);
+    }
+
+    if (written == -1) {
+      PR_LOG(gAudioStreamLog, PR_LOG_ERROR, ("nsNativeAudioStream: sa_stream_write error"));
+      mInError = true;
+    }
+
+    NS_ASSERTION(mTimeStretcher->numSamples() == 0,
+                 "We did not get all the data from the SoundTouch pipeline.");
+  }
+
   if (mInError)
     return;
 
   int r = sa_stream_drain(static_cast<sa_stream_t*>(mAudioHandle));
   if (r != SA_SUCCESS && r != SA_ERROR_INVALID) {
     PR_LOG(gAudioStreamLog, PR_LOG_ERROR, ("nsNativeAudioStream: sa_stream_drain error"));
     mInError = true;
   }
@@ -349,25 +473,26 @@ void nsNativeAudioStream::Resume()
   if (mInError)
     return;
   mPaused = false;
   sa_stream_resume(static_cast<sa_stream_t*>(mAudioHandle));
 }
 
 int64_t nsNativeAudioStream::GetPosition()
 {
-  int64_t position = GetPositionInFrames();
-  if (position >= 0) {
-    return ((USECS_PER_S * position) / mRate);
-  }
-  return -1;
+  return mAudioClock.GetPosition();
 }
 
 int64_t nsNativeAudioStream::GetPositionInFrames()
 {
+  return mAudioClock.GetPositionInFrames();
+}
+
+int64_t nsNativeAudioStream::GetPositionInFramesInternal()
+{
   if (mInError) {
     return -1;
   }
 
   sa_position_t positionType = SA_POSITION_WRITE_SOFTWARE;
 #if defined(XP_WIN)
   positionType = SA_POSITION_WRITE_HARDWARE;
 #endif
@@ -476,16 +601,17 @@ class nsBufferedAudioStream : public Aud
   nsresult Write(const AudioDataValue* aBuf, uint32_t aFrames);
   uint32_t Available();
   void SetVolume(double aVolume);
   void Drain();
   void Pause();
   void Resume();
   int64_t GetPosition();
   int64_t GetPositionInFrames();
+  int64_t GetPositionInFramesInternal();
   bool IsPaused();
   int32_t GetMinWriteSize();
 
 private:
   static long DataCallback_S(cubeb_stream*, void* aThis, void* aBuffer, long aFrames)
   {
     return static_cast<nsBufferedAudioStream*>(aThis)->DataCallback(aBuffer, aFrames);
   }
@@ -493,16 +619,21 @@ private:
   static void StateCallback_S(cubeb_stream*, void* aThis, cubeb_state aState)
   {
     static_cast<nsBufferedAudioStream*>(aThis)->StateCallback(aState);
   }
 
   long DataCallback(void* aBuffer, long aFrames);
   void StateCallback(cubeb_state aState);
 
+  long GetUnprocessed(void* aBuffer, long aFrames);
+
+  long GetTimeStretched(void* aBuffer, long aFrames);
+
+
   // Shared implementation of underflow adjusted position calculation.
   // Caller must own the monitor.
   int64_t GetPositionInFramesUnlocked();
 
   // The monitor is held to protect all access to member variables.  Write()
   // waits while mBuffer is full; DataCallback() notifies as it consumes
   // data from mBuffer.  Drain() waits while mState is DRAINING;
   // StateCallback() notifies when mState is DRAINED.
@@ -522,16 +653,27 @@ private:
   double mVolume;
 
   // Owning reference to a cubeb_stream.  cubeb_stream_destroy is called by
   // nsAutoRef's destructor.
   nsAutoRef<cubeb_stream> mCubebStream;
 
   uint32_t mBytesPerFrame;
 
+  uint32_t BytesToFrames(uint32_t aBytes) {
+    NS_ASSERTION(aBytes % mBytesPerFrame == 0,
+                 "Byte count not aligned on frames size.");
+    return aBytes / mBytesPerFrame;
+  }
+
+  uint32_t FramesToBytes(uint32_t aFrames) {
+    return aFrames * mBytesPerFrame;
+  }
+
+
   enum StreamState {
     INITIALIZED, // Initialized, playback has not begun.
     STARTED,     // Started by a call to Write() (iff INITIALIZED) or Resume().
     STOPPED,     // Stopped by a call to Pause().
     DRAINING,    // Drain requested.  DataCallback will indicate end of stream
                  // once the remaining contents of mBuffer are requested by
                  // cubeb, after which StateCallback will indicate drain
                  // completion.
@@ -570,45 +712,47 @@ nsBufferedAudioStream::Init(int32_t aNum
                             const dom::AudioChannelType aAudioChannelType)
 {
   cubeb* cubebContext = GetCubebContext();
 
   if (!cubebContext || aNumChannels < 0 || aRate < 0) {
     return NS_ERROR_FAILURE;
   }
 
-  mRate = aRate;
+  mInRate = mOutRate = aRate;
   mChannels = aNumChannels;
 
   cubeb_stream_params params;
   params.rate = aRate;
   params.channels = aNumChannels;
   if (AUDIO_OUTPUT_FORMAT == AUDIO_FORMAT_S16) {
     params.format = CUBEB_SAMPLE_S16NE;
   } else {
     params.format = CUBEB_SAMPLE_FLOAT32NE;
   }
   mBytesPerFrame = sizeof(AudioDataValue) * aNumChannels;
 
+  mAudioClock.Init();
+
   {
     cubeb_stream* stream;
     if (cubeb_stream_init(cubebContext, &stream, "nsBufferedAudioStream", params,
                           GetCubebLatency(), DataCallback_S, StateCallback_S, this) == CUBEB_OK) {
       mCubebStream.own(stream);
     }
   }
 
   if (!mCubebStream) {
     return NS_ERROR_FAILURE;
   }
 
   // Size mBuffer for one second of audio.  This value is arbitrary, and was
   // selected based on the observed behaviour of the existing AudioStream
   // implementations.
-  uint32_t bufferLimit = aRate * mBytesPerFrame;
+  uint32_t bufferLimit = FramesToBytes(aRate);
   NS_ABORT_IF_FALSE(bufferLimit % mBytesPerFrame == 0, "Must buffer complete frames");
   mBuffer.SetCapacity(bufferLimit);
 
   return NS_OK;
 }
 
 void
 nsBufferedAudioStream::Shutdown()
@@ -623,24 +767,26 @@ nsBufferedAudioStream::Shutdown()
 
 nsresult
 nsBufferedAudioStream::Write(const AudioDataValue* aBuf, uint32_t aFrames)
 {
   MonitorAutoLock mon(mMonitor);
   if (!mCubebStream || mState == ERRORED) {
     return NS_ERROR_FAILURE;
   }
-  NS_ASSERTION(mState == INITIALIZED || mState == STARTED, "Stream write in unexpected state.");
+  NS_ASSERTION(mState == INITIALIZED || mState == STARTED,
+    "Stream write in unexpected state.");
 
   const uint8_t* src = reinterpret_cast<const uint8_t*>(aBuf);
-  uint32_t bytesToCopy = aFrames * mBytesPerFrame;
+  uint32_t bytesToCopy = FramesToBytes(aFrames);
 
   while (bytesToCopy > 0) {
     uint32_t available = NS_MIN(bytesToCopy, mBuffer.Available());
-    NS_ABORT_IF_FALSE(available % mBytesPerFrame == 0, "Must copy complete frames.");
+    NS_ABORT_IF_FALSE(available % mBytesPerFrame == 0,
+        "Must copy complete frames.");
 
     mBuffer.AppendElements(src, available);
     src += available;
     bytesToCopy -= available;
 
     if (mState != STARTED) {
       int r;
       {
@@ -662,17 +808,17 @@ nsBufferedAudioStream::Write(const Audio
   return NS_OK;
 }
 
 uint32_t
 nsBufferedAudioStream::Available()
 {
   MonitorAutoLock mon(mMonitor);
   NS_ABORT_IF_FALSE(mBuffer.Length() % mBytesPerFrame == 0, "Buffer invariant violated.");
-  return mBuffer.Available() / mBytesPerFrame;
+  return BytesToFrames(mBuffer.Available());
 }
 
 int32_t
 nsBufferedAudioStream::GetMinWriteSize()
 {
   return 1;
 }
 
@@ -731,39 +877,40 @@ nsBufferedAudioStream::Resume()
   if (mState != ERRORED && r == CUBEB_OK) {
     mState = STARTED;
   }
 }
 
 int64_t
 nsBufferedAudioStream::GetPosition()
 {
-  MonitorAutoLock mon(mMonitor);
-  int64_t frames = GetPositionInFramesUnlocked();
-  if (frames >= 0) {
-    return USECS_PER_S * frames / mRate;
-  }
-  return -1;
+  return mAudioClock.GetPosition();
 }
 
 // This function is miscompiled by PGO with MSVC 2010.  See bug 768333.
 #ifdef _MSC_VER
 #pragma optimize("", off)
 #endif
 int64_t
 nsBufferedAudioStream::GetPositionInFrames()
 {
-  MonitorAutoLock mon(mMonitor);
-  return GetPositionInFramesUnlocked();
+  return mAudioClock.GetPositionInFrames();
 }
 #ifdef _MSC_VER
 #pragma optimize("", on)
 #endif
 
 int64_t
+nsBufferedAudioStream::GetPositionInFramesInternal()
+{
+  MonitorAutoLock mon(mMonitor);
+  return GetPositionInFramesUnlocked();
+}
+
+int64_t
 nsBufferedAudioStream::GetPositionInFramesUnlocked()
 {
   mMonitor.AssertCurrentThreadOwns();
 
   if (!mCubebStream || mState == ERRORED) {
     return -1;
   }
 
@@ -787,70 +934,221 @@ nsBufferedAudioStream::GetPositionInFram
 bool
 nsBufferedAudioStream::IsPaused()
 {
   MonitorAutoLock mon(mMonitor);
   return mState == STOPPED;
 }
 
 long
+nsBufferedAudioStream::GetUnprocessed(void* aBuffer, long aFrames)
+{
+  uint8_t* wpos = reinterpret_cast<uint8_t*>(aBuffer);
+
+  // Flush the timestretcher pipeline, if we were playing using a playback rate
+  // other than 1.0.
+  uint32_t flushedFrames = 0;
+  if (mTimeStretcher && mTimeStretcher->numSamples()) {
+    flushedFrames = mTimeStretcher->receiveSamples(reinterpret_cast<AudioDataValue*>(wpos), aFrames);
+    wpos += FramesToBytes(flushedFrames);
+  }
+  uint32_t toPopBytes = FramesToBytes(aFrames - flushedFrames);
+  uint32_t available = NS_MIN(toPopBytes, mBuffer.Length());
+
+  void* input[2];
+  uint32_t input_size[2];
+  mBuffer.PopElements(available, &input[0], &input_size[0], &input[1], &input_size[1]);
+  memcpy(wpos, input[0], input_size[0]);
+  wpos += input_size[0];
+  memcpy(wpos, input[1], input_size[1]);
+  return BytesToFrames(available) + flushedFrames;
+}
+
+long
+nsBufferedAudioStream::GetTimeStretched(void* aBuffer, long aFrames)
+{
+  long processedFrames = 0;
+  if (!EnsureTimeStretcherInitialized()) {
+    return -1;
+  }
+  uint8_t* wpos = reinterpret_cast<uint8_t*>(aBuffer);
+  double playbackRate = static_cast<double>(mInRate) / mOutRate;
+  uint32_t toPopBytes = FramesToBytes(ceil(aFrames / playbackRate));
+  uint32_t available = 0;
+  bool lowOnBufferedData = false;
+  do {
+    // Check if we already have enough data in the time stretcher pipeline.
+    if (mTimeStretcher->numSamples() <= static_cast<uint32_t>(aFrames)) {
+      void* input[2];
+      uint32_t input_size[2];
+      available = NS_MIN(mBuffer.Length(), toPopBytes);
+      if (available != toPopBytes) {
+        lowOnBufferedData = true;
+      }
+      mBuffer.PopElements(available, &input[0], &input_size[0],
+                                     &input[1], &input_size[1]);
+      for(uint32_t i = 0; i < 2; i++) {
+        mTimeStretcher->putSamples(reinterpret_cast<AudioDataValue*>(input[i]), BytesToFrames(input_size[i]));
+      }
+    }
+    uint32_t receivedFrames = mTimeStretcher->receiveSamples(reinterpret_cast<AudioDataValue*>(wpos), aFrames - processedFrames);
+    wpos += FramesToBytes(receivedFrames);
+    processedFrames += receivedFrames;
+  } while (processedFrames < aFrames && !lowOnBufferedData);
+
+  return processedFrames;
+}
+
+long
 nsBufferedAudioStream::DataCallback(void* aBuffer, long aFrames)
 {
   MonitorAutoLock mon(mMonitor);
-  uint32_t bytesWanted = aFrames * mBytesPerFrame;
+  uint32_t available = NS_MIN(static_cast<uint32_t>(FramesToBytes(aFrames)), mBuffer.Length());
+  NS_ABORT_IF_FALSE(available % mBytesPerFrame == 0, "Must copy complete frames");
+  uint32_t underrunFrames = 0;
+  uint32_t servicedFrames = 0;
 
-  // Adjust bytesWanted to fit what is available in mBuffer.
-  uint32_t available = NS_MIN(bytesWanted, mBuffer.Length());
-  NS_ABORT_IF_FALSE(available % mBytesPerFrame == 0, "Must copy complete frames");
-
-  if (available > 0) {
-    // Copy each sample from mBuffer to aBuffer, adjusting the volume during the copy.
+  if (available) {
+    AudioDataValue* output = reinterpret_cast<AudioDataValue*>(aBuffer);
+    if (mInRate == mOutRate) {
+      servicedFrames = GetUnprocessed(output, aFrames);
+    } else {
+      servicedFrames = GetTimeStretched(output, aFrames);
+    }
     float scaled_volume = float(GetVolumeScale() * mVolume);
 
-    // Fetch input pointers from the ring buffer.
-    void* input[2];
-    uint32_t input_size[2];
-    mBuffer.PopElements(available, &input[0], &input_size[0], &input[1], &input_size[1]);
-
-    uint8_t* output = static_cast<uint8_t*>(aBuffer);
-    for (int i = 0; i < 2; ++i) {
-      const AudioDataValue* src = static_cast<const AudioDataValue*>(input[i]);
-      AudioDataValue* dst = reinterpret_cast<AudioDataValue*>(output);
-
-      ConvertAudioSamplesWithScale(src, dst, input_size[i]/sizeof(AudioDataValue),
-                                   scaled_volume);
-      output += input_size[i];
-    }
+    ScaleAudioSamples(output, aFrames * mChannels, scaled_volume);
 
     NS_ABORT_IF_FALSE(mBuffer.Length() % mBytesPerFrame == 0, "Must copy complete frames");
 
     // Notify any blocked Write() call that more space is available in mBuffer.
     mon.NotifyAll();
-
-    // Calculate remaining bytes requested by caller.  If the stream is not
-    // draining an underrun has occurred, so fill the remaining buffer with
-    // silence.
-    bytesWanted -= available;
   }
 
+  underrunFrames = aFrames - servicedFrames;
+
   if (mState != DRAINING) {
-    memset(static_cast<uint8_t*>(aBuffer) + available, 0, bytesWanted);
-    mLostFrames += bytesWanted / mBytesPerFrame;
-    bytesWanted = 0;
+    uint8_t* rpos = static_cast<uint8_t*>(aBuffer) + FramesToBytes(aFrames - underrunFrames);
+    memset(rpos, 0, FramesToBytes(underrunFrames));
+    mLostFrames += underrunFrames;
+    servicedFrames += underrunFrames;
   }
 
-  return aFrames - (bytesWanted / mBytesPerFrame);
+  mAudioClock.UpdateWritePosition(servicedFrames);
+  return servicedFrames;
 }
 
 void
 nsBufferedAudioStream::StateCallback(cubeb_state aState)
 {
   MonitorAutoLock mon(mMonitor);
   if (aState == CUBEB_STATE_DRAINED) {
     mState = DRAINED;
   } else if (aState == CUBEB_STATE_ERROR) {
     mState = ERRORED;
   }
   mon.NotifyAll();
 }
+
 #endif
 
+AudioClock::AudioClock(AudioStream* aStream)
+ :mAudioStream(aStream),
+  mOldOutRate(0),
+  mBasePosition(0),
+  mBaseOffset(0),
+  mOldBaseOffset(0),
+  mPlaybackRateChangeOffset(0),
+  mPreviousPosition(0),
+  mWritten(0),
+  mOutRate(0),
+  mInRate(0),
+  mPreservesPitch(true),
+  mPlaybackRate(1.0),
+  mCompensatingLatency(false)
+{}
+
+void AudioClock::Init()
+{
+  mOutRate = mAudioStream->GetRate();
+  mInRate = mAudioStream->GetRate();
+  mPlaybackRate = 1.0;
+}
+
+void AudioClock::UpdateWritePosition(uint32_t aCount)
+{
+  mWritten += aCount;
+}
+
+uint64_t AudioClock::GetPosition()
+{
+  NS_ASSERTION(mInRate != 0 && mOutRate != 0, "AudioClock not initialized.");
+  int64_t position = mAudioStream->GetPositionInFramesInternal();
+  int64_t diffOffset;
+  if (position >= 0) {
+    if (position < mPlaybackRateChangeOffset) {
+      // See if we are still playing frames pushed with the old playback rate in
+      // the backend. If we are, use the old output rate to compute the
+      // position.
+      mCompensatingLatency = true;
+      diffOffset = position - mOldBaseOffset;
+      position = static_cast<uint64_t>(mOldBasePosition +
+        static_cast<float>(USECS_PER_S * diffOffset) / mOldOutRate);
+      mPreviousPosition = position;
+      return position;
+    }
+
+    if (mCompensatingLatency) {
+      diffOffset = position - mPlaybackRateChangeOffset;
+      mCompensatingLatency = false;
+      mBasePosition = mPreviousPosition;
+    } else {
+      diffOffset = position - mPlaybackRateChangeOffset;
+    }
+    position =  static_cast<uint64_t>(mBasePosition +
+      (static_cast<float>(USECS_PER_S * diffOffset) / mOutRate));
+    return position;
+  }
+  return -1;
+}
+
+uint64_t AudioClock::GetPositionInFrames()
+{
+  return (GetPosition() * mOutRate) / USECS_PER_S;
+}
+
+void AudioClock::SetPlaybackRate(double aPlaybackRate)
+{
+  int64_t position = mAudioStream->GetPositionInFramesInternal();
+  if (position > mPlaybackRateChangeOffset) {
+    mOldBasePosition = mBasePosition;
+    mBasePosition = GetPosition();
+    mOldBaseOffset = mPlaybackRateChangeOffset;
+    mBaseOffset = position;
+    mPlaybackRateChangeOffset = mWritten;
+    mOldOutRate = mOutRate;
+    mOutRate = static_cast<int>(mInRate / aPlaybackRate);
+  } else {
+    // The playbackRate has been changed before the end of the latency
+    // compensation phase. We don't update the mOld* variable. That way, the
+    // last playbackRate set is taken into account.
+    mBasePosition = GetPosition();
+    mBaseOffset = position;
+    mPlaybackRateChangeOffset = mWritten;
+    mOutRate = static_cast<int>(mInRate / aPlaybackRate);
+  }
+}
+
+double AudioClock::GetPlaybackRate()
+{
+  return mPlaybackRate;
+}
+
+void AudioClock::SetPreservesPitch(bool aPreservesPitch)
+{
+  mPreservesPitch = aPreservesPitch;
+}
+
+bool AudioClock::GetPreservesPitch()
+{
+  return mPreservesPitch;
+}
 } // namespace mozilla
--- a/content/media/AudioStream.h
+++ b/content/media/AudioStream.h
@@ -4,30 +4,107 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 #if !defined(AudioStream_h_)
 #define AudioStream_h_
 
 #include "nscore.h"
 #include "AudioSampleFormat.h"
 #include "AudioChannelCommon.h"
+#include "soundtouch/SoundTouch.h"
+#include "nsAutoRef.h"
+
+
+template <>
+class nsAutoRefTraits<soundtouch::SoundTouch> : public nsPointerRefTraits<soundtouch::SoundTouch>
+{
+public:
+  static void Release(soundtouch::SoundTouch* resamplerState) {
+    delete resamplerState;
+    resamplerState = nullptr;
+  }
+};
 
 namespace mozilla {
 
+class AudioStream;
+
+class AudioClock
+{
+  public:
+    AudioClock(mozilla::AudioStream* aStream);
+    // Initialize the clock with the current AudioStream. Need to be called
+    // before querying the clock. Called on the audio thread.
+    void Init();
+    // Update the number of samples that has been written in the audio backend.
+    // Called on the state machine thread.
+    void UpdateWritePosition(uint32_t aCount);
+    // Get the read position of the stream, in microseconds.
+    // Called on the state machine thead.
+    uint64_t GetPosition();
+    // Get the read position of the stream, in frames.
+    // Called on the state machine thead.
+    uint64_t GetPositionInFrames();
+    // Set the playback rate.
+    // Called on the audio thread.
+    void SetPlaybackRate(double aPlaybackRate);
+    // Get the current playback rate.
+    // Called on the audio thread.
+    double GetPlaybackRate();
+    // Set if we are preserving the pitch.
+    // Called on the audio thread.
+    void SetPreservesPitch(bool aPreservesPitch);
+    // Get the current pitch preservation state.
+    // Called on the audio thread.
+    bool GetPreservesPitch();
+  private:
+    // This AudioStream holds a strong reference to this AudioClock. This
+    // pointer is garanteed to always be valid.
+    AudioStream* mAudioStream;
+    // The old output rate, to compensate audio latency for the period inbetween
+    // the moment resampled buffers are pushed to the hardware and the moment the
+    // clock should take the new rate into account for A/V sync.
+    int mOldOutRate;
+    // Position at which the last playback rate change occured
+    int64_t mBasePosition;
+    // Offset, in frames, at which the last playback rate change occured
+    int64_t mBaseOffset;
+    // Old base offset (number of samples), used when changing rate to compute the
+    // position in the stream.
+    int64_t mOldBaseOffset;
+    // Old base position (number of microseconds), when changing rate. This is the
+    // time in the media, not wall clock position.
+    int64_t mOldBasePosition;
+    // Write position at which the playbackRate change occured.
+    int64_t mPlaybackRateChangeOffset;
+    // The previous position reached in the media, used when compensating
+    // latency, to have the position at which the playbackRate change occured.
+    int64_t mPreviousPosition;
+    // Number of samples effectivelly written in backend, i.e. write position.
+    int64_t mWritten;
+    // Output rate in Hz (characteristic of the playback rate)
+    int mOutRate;
+    // Input rate in Hz (characteristic of the media being played)
+    int mInRate;
+    // True if the we are timestretching, false if we are resampling.
+    bool mPreservesPitch;
+    // The current playback rate.
+    double mPlaybackRate;
+    // True if we are playing at the old playbackRate after it has been changed.
+    bool mCompensatingLatency;
+};
+
 // Access to a single instance of this class must be synchronized by
 // callers, or made from a single thread.  One exception is that access to
 // GetPosition, GetPositionInFrames, SetVolume, and Get{Rate,Channels}
 // is thread-safe without external synchronization.
 class AudioStream
 {
 public:
-  AudioStream()
-    : mRate(0),
-      mChannels(0)
-  {}
+  AudioStream();
 
   virtual ~AudioStream();
 
   // Initialize Audio Library. Some Audio backends require initializing the
   // library before using it.
   static void InitLibrary();
 
   // Shutdown Audio Library. Some Audio backends require shutting down the
@@ -66,33 +143,52 @@ public:
 
   // Pause audio playback
   virtual void Pause() = 0;
 
   // Resume audio playback
   virtual void Resume() = 0;
 
   // Return the position in microseconds of the audio frame being played by
-  // the audio hardware.  Thread-safe.
+  // the audio hardware, compensated for playback rate change. Thread-safe.
   virtual int64_t GetPosition() = 0;
 
   // Return the position, measured in audio frames played since the stream
   // was opened, of the audio hardware.  Thread-safe.
   virtual int64_t GetPositionInFrames() = 0;
 
+  // Return the position, measured in audio framed played since the stream was
+  // opened, of the audio hardware, not adjusted for the changes of playback
+  // rate.
+  virtual int64_t GetPositionInFramesInternal() = 0;
+
   // Returns true when the audio stream is paused.
   virtual bool IsPaused() = 0;
 
   // Returns the minimum number of audio frames which must be written before
   // you can be sure that something will be played.
   virtual int32_t GetMinWriteSize() = 0;
 
-  int GetRate() { return mRate; }
+  int GetRate() { return mOutRate; }
   int GetChannels() { return mChannels; }
 
+  // This should be called before attempting to use the time stretcher. It
+  // return false in case of error.
+  bool EnsureTimeStretcherInitialized();
+  // Set playback rate as a multiple of the intrinsic playback rate. This is to
+  // be called only with aPlaybackRate > 0.0.
+  virtual nsresult SetPlaybackRate(double aPlaybackRate);
+  // Switch between resampling (if false) and time stretching (if true, default).
+  virtual nsresult SetPreservesPitch(bool aPreservesPitch);
+
 protected:
-  int mRate;
+  // Input rate in Hz (characteristic of the media being played)
+  int mInRate;
+  // Output rate in Hz (characteristic of the playback rate)
+  int mOutRate;
   int mChannels;
+  AudioClock mAudioClock;
+  nsAutoRef<soundtouch::SoundTouch> mTimeStretcher;
 };
 
 } // namespace mozilla
 
 #endif
--- a/content/media/MediaDecoder.cpp
+++ b/content/media/MediaDecoder.cpp
@@ -1307,16 +1307,43 @@ void MediaDecoder::NotifyAudioAvailableL
 {
   MOZ_ASSERT(NS_IsMainThread());
   if (mDecoderStateMachine) {
     ReentrantMonitorAutoEnter mon(GetReentrantMonitor());
     mDecoderStateMachine->NotifyAudioAvailableListener();
   }
 }
 
+void MediaDecoder::SetPlaybackRate(double aPlaybackRate)
+{
+  if (aPlaybackRate == 0) {
+    mPausedForPlaybackRateNull = true;
+    Pause();
+    return;
+  } else if (mPausedForPlaybackRateNull) {
+    // If the playbackRate is no longer null, restart the playback, iff the
+    // media was playing.
+    if (mOwner && !mOwner->GetPaused()) {
+      Play();
+    }
+    mPausedForPlaybackRateNull = false;
+  }
+
+  if (mDecoderStateMachine) {
+    mDecoderStateMachine->SetPlaybackRate(aPlaybackRate);
+  }
+}
+
+void MediaDecoder::SetPreservesPitch(bool aPreservesPitch)
+{
+  if (mDecoderStateMachine) {
+    mDecoderStateMachine->SetPreservesPitch(aPreservesPitch);
+  }
+}
+
 bool MediaDecoder::OnDecodeThread() const {
   return mDecoderStateMachine->OnDecodeThread();
 }
 
 ReentrantMonitor& MediaDecoder::GetReentrantMonitor() {
   return mReentrantMonitor.GetReentrantMonitor();
 }
 
--- a/content/media/MediaDecoder.h
+++ b/content/media/MediaDecoder.h
@@ -328,16 +328,19 @@ public:
   // Pause video playback.
   virtual void Pause();
   // Adjust the speed of the playback, optionally with pitch correction,
   virtual void SetVolume(double aVolume);
   // Sets whether audio is being captured. If it is, we won't play any
   // of our audio.
   virtual void SetAudioCaptured(bool aCaptured);
 
+  void SetPlaybackRate(double aPlaybackRate);
+  void SetPreservesPitch(bool aPreservesPitch);
+
   // All MediaStream-related data is protected by mReentrantMonitor.
   // We have at most one DecodedStreamData per MediaDecoder. Its stream
   // is used as the input for each ProcessedMediaStream created by calls to
   // captureStream(UntilEnded). Seeking creates a new source stream, as does
   // replaying after the input as ended. In the latter case, the new source is
   // not connected to streams created by captureStreamUntilEnded.
 
   struct DecodedStreamData {
@@ -1043,16 +1046,19 @@ protected:
   bool mPinnedForSeek;
 
   // True if the decoder is being shutdown. At this point all events that
   // are currently queued need to return immediately to prevent javascript
   // being run that operates on the element and decoder during shutdown.
   // Read/Write from the main thread only.
   bool mShuttingDown;
 
+  // True if the playback is paused because the playback rate member is 0.0.
+  bool mPausedForPlaybackRateNull;
+
   // Be assigned from media element during the initialization and pass to
   // AudioStream Class.
   AudioChannelType mAudioChannelType;
 };
 
 } // namespace mozilla
 
 #endif
--- a/content/media/MediaDecoderStateMachine.cpp
+++ b/content/media/MediaDecoderStateMachine.cpp
@@ -282,17 +282,17 @@ bool StateMachineTracker::IsQueued(Media
     if (m == aStateMachine) {
       return true;
     }
   }
   return false;
 }
 #endif
 
-void StateMachineTracker::CleanupGlobalStateMachine() 
+void StateMachineTracker::CleanupGlobalStateMachine()
 {
   NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
   NS_ABORT_IF_FALSE(mStateMachineCount > 0,
     "State machine ref count must be > 0");
   mStateMachineCount--;
   if (mStateMachineCount == 0) {
     LOG(PR_LOG_DEBUG, ("Destroying media state machine thread"));
     NS_ASSERTION(mPending.GetSize() == 0, "Shouldn't all requests be handled by now?");
@@ -373,27 +373,31 @@ nsresult StateMachineTracker::RequestCre
   return NS_OK;
 }
 
 MediaDecoderStateMachine::MediaDecoderStateMachine(MediaDecoder* aDecoder,
                                                            MediaDecoderReader* aReader,
                                                            bool aRealTime) :
   mDecoder(aDecoder),
   mState(DECODER_STATE_DECODING_METADATA),
+  mResetPlayStartTime(false),
   mPlayDuration(0),
   mStartTime(-1),
   mEndTime(-1),
   mSeekTime(0),
   mFragmentEndTime(-1),
   mReader(aReader),
   mCurrentFrameTime(0),
   mAudioStartTime(-1),
   mAudioEndTime(-1),
   mVideoFrameEndTime(-1),
   mVolume(1.0),
+  mPlaybackRate(1.0),
+  mPreservesPitch(true),
+  mBasePosition(0),
   mAudioCaptured(false),
   mSeekable(true),
   mPositionChangeQueued(false),
   mAudioCompleted(false),
   mGotDurationFromMetaData(false),
   mStopDecodeThread(true),
   mDecodeThreadIdle(false),
   mStopAudioThread(true),
@@ -459,17 +463,17 @@ bool MediaDecoderStateMachine::HasFuture
   mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
   NS_ASSERTION(HasAudio(), "Should only call HasFutureAudio() when we have audio");
   // We've got audio ready to play if:
   // 1. We've not completed playback of audio, and
   // 2. we either have more than the threshold of decoded audio available, or
   //    we've completely decoded all audio (but not finished playing it yet
   //    as per 1).
   return !mAudioCompleted &&
-         (AudioDecodedUsecs() > LOW_AUDIO_USECS || mReader->AudioQueue().IsFinished());
+         (AudioDecodedUsecs() > LOW_AUDIO_USECS * mPlaybackRate || mReader->AudioQueue().IsFinished());
 }
 
 bool MediaDecoderStateMachine::HaveNextFrameData() const {
   mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
   return (!HasAudio() || HasFutureAudio()) &&
          (!HasVideo() || mReader->VideoQueue().GetSize() > 0);
 }
 
@@ -750,17 +754,17 @@ bool MediaDecoderStateMachine::HaveEnoug
 
   return true;
 }
 
 bool MediaDecoderStateMachine::HaveEnoughDecodedVideo()
 {
   mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
 
-  if (static_cast<uint32_t>(mReader->VideoQueue().GetSize()) < mAmpleVideoFrames) {
+  if (static_cast<uint32_t>(mReader->VideoQueue().GetSize()) < mAmpleVideoFrames * mPlaybackRate) {
     return false;
   }
 
   DecodedStreamData* stream = mDecoder->GetDecodedStream();
   if (stream && stream->mStreamInitialized && !stream->mHaveSentFinishVideo) {
     if (!stream->mStream->HaveEnoughBuffered(TRACK_VIDEO)) {
       return false;
     }
@@ -814,41 +818,42 @@ void MediaDecoderStateMachine::DecodeLoo
          !mStopDecodeThread &&
          (videoPlaying || audioPlaying))
   {
     // We don't want to consider skipping to the next keyframe if we've
     // only just started up the decode loop, so wait until we've decoded
     // some frames before enabling the keyframe skip logic on video.
     if (videoPump &&
         (static_cast<uint32_t>(mReader->VideoQueue().GetSize())
-         >= videoPumpThreshold))
+         >= videoPumpThreshold * mPlaybackRate))
     {
       videoPump = false;
     }
 
     // We don't want to consider skipping to the next keyframe if we've
     // only just started up the decode loop, so wait until we've decoded
     // some audio data before enabling the keyframe skip logic on audio.
-    if (audioPump && GetDecodedAudioDuration() >= audioPumpThreshold) {
+    if (audioPump && GetDecodedAudioDuration() >= audioPumpThreshold * mPlaybackRate) {
       audioPump = false;
     }
 
     // We'll skip the video decode to the nearest keyframe if we're low on
     // audio, or if we're low on video, provided we're not running low on
     // data to decode. If we're running low on downloaded data to decode,
     // we won't start keyframe skipping, as we'll be pausing playback to buffer
     // soon anyway and we'll want to be able to display frames immediately
     // after buffering finishes.
     if (mState == DECODER_STATE_DECODING &&
         !skipToNextKeyframe &&
         videoPlaying &&
-        ((!audioPump && audioPlaying && !mDidThrottleAudioDecoding && GetDecodedAudioDuration() < lowAudioThreshold) ||
+        ((!audioPump && audioPlaying && !mDidThrottleAudioDecoding &&
+          GetDecodedAudioDuration() < lowAudioThreshold * mPlaybackRate) ||
          (!videoPump && videoPlaying && !mDidThrottleVideoDecoding &&
           (static_cast<uint32_t>(mReader->VideoQueue().GetSize())
-           < LOW_VIDEO_FRAMES))) &&
+           < LOW_VIDEO_FRAMES * mPlaybackRate))) &&
         !HasLowUndecodedData())
     {
       skipToNextKeyframe = true;
       LOG(PR_LOG_DEBUG, ("%p Skipping video decode to the next keyframe", mDecoder.get()));
     }
 
     // Video decode.
     bool throttleVideoDecoding = !videoPlaying || HaveEnoughDecodedVideo();
@@ -878,17 +883,17 @@ void MediaDecoderStateMachine::DecodeLoo
                                      ampleAudioThreshold);
         LOG(PR_LOG_DEBUG,
             ("Slow video decode, set lowAudioThreshold=%lld ampleAudioThreshold=%lld",
              lowAudioThreshold, ampleAudioThreshold));
       }
     }
 
     // Audio decode.
-    bool throttleAudioDecoding = !audioPlaying || HaveEnoughDecodedAudio(ampleAudioThreshold);
+    bool throttleAudioDecoding = !audioPlaying || HaveEnoughDecodedAudio(ampleAudioThreshold * mPlaybackRate);
     if (mDidThrottleAudioDecoding && !throttleAudioDecoding) {
       audioPump = true;
     }
     mDidThrottleAudioDecoding = throttleAudioDecoding;
     if (!mDidThrottleAudioDecoding) {
       ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor());
       audioPlaying = mReader->DecodeAudioData();
     }
@@ -962,30 +967,42 @@ void MediaDecoderStateMachine::AudioLoop
 {
   NS_ASSERTION(OnAudioThread(), "Should be on audio thread.");
   LOG(PR_LOG_DEBUG, ("%p Begun audio thread/loop", mDecoder.get()));
   int64_t audioDuration = 0;
   int64_t audioStartTime = -1;
   uint32_t channels, rate;
   double volume = -1;
   bool setVolume;
+  double playbackRate = -1;
+  bool setPlaybackRate;
+  bool preservesPitch;
+  bool setPreservesPitch;
   int32_t minWriteFrames = -1;
   {
     ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
     mAudioCompleted = false;
     audioStartTime = mAudioStartTime;
     channels = mInfo.mAudioChannels;
     rate = mInfo.mAudioRate;
     NS_ASSERTION(audioStartTime != -1, "Should have audio start time by now");
 
     mAudioStream = AudioStream::AllocateStream();
     mAudioStream->Init(channels, rate, mDecoder->GetAudioChannelType());
 
     volume = mVolume;
     mAudioStream->SetVolume(volume);
+    preservesPitch = mPreservesPitch;
+    mAudioStream->SetPreservesPitch(preservesPitch);
+    playbackRate = mPlaybackRate;
+    if (playbackRate != 1.0) {
+      NS_ASSERTION(playbackRate != 0,
+          "Don't set the playbackRate to 0 on an AudioStream.");
+      mAudioStream->SetPlaybackRate(playbackRate);
+    }
   }
   while (1) {
 
     // Wait while we're not playing, and we're not shutting down, or we're
     // playing and we've got no audio to play.
     {
       ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
       NS_ASSERTION(mState != DECODER_STATE_DECODING_METADATA,
@@ -1012,24 +1029,40 @@ void MediaDecoderStateMachine::AudioLoop
         break;
       }
 
       // We only want to go to the expense of changing the volume if
       // the volume has changed.
       setVolume = volume != mVolume;
       volume = mVolume;
 
+      // Same for the playbackRate.
+      setPlaybackRate = playbackRate != mPlaybackRate;
+      playbackRate = mPlaybackRate;
+
+      // Same for the pitch preservation.
+      setPreservesPitch = preservesPitch != mPreservesPitch;
+      preservesPitch = mPreservesPitch;
+
       if (IsPlaying() && mAudioStream->IsPaused()) {
         mAudioStream->Resume();
       }
     }
 
     if (setVolume) {
       mAudioStream->SetVolume(volume);
     }
+    if (setPlaybackRate) {
+      NS_ASSERTION(playbackRate != 0,
+                   "Don't set the playbackRate to 0 in the AudioStreams");
+      mAudioStream->SetPlaybackRate(playbackRate);
+    }
+    if (setPreservesPitch) {
+      mAudioStream->SetPreservesPitch(preservesPitch);
+    }
     if (minWriteFrames == -1) {
       minWriteFrames = mAudioStream->GetMinWriteSize();
     }
     NS_ASSERTION(mReader->AudioQueue().GetSize() > 0,
                  "Should have data to play");
     // See if there's a gap in the audio. If there is, push silence into the
     // audio hardware, so we can play across the gap.
     const AudioData* s = mReader->AudioQueue().PeekFront();
@@ -1202,17 +1235,17 @@ nsresult MediaDecoderStateMachine::Init(
   return mReader->Init(cloneReader);
 }
 
 void MediaDecoderStateMachine::StopPlayback()
 {
   LOG(PR_LOG_DEBUG, ("%p StopPlayback()", mDecoder.get()));
 
   NS_ASSERTION(OnStateMachineThread() || OnDecodeThread(),
-               "Should be on state machine thread.");
+               "Should be on state machine thread or the decoder thread.");
   mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
 
   mDecoder->mPlaybackStatistics.Stop(TimeStamp::Now());
 
   if (IsPlaying()) {
     mPlayDuration += DurationToUsecs(TimeStamp::Now() - mPlayStartTime);
     mPlayStartTime = TimeStamp();
   }
@@ -1472,16 +1505,17 @@ void MediaDecoderStateMachine::Seek(doub
   NS_ASSERTION(mSeekTime >= mStartTime && mSeekTime <= mEndTime,
                "Can only seek in range [0,duration]");
 
   // Bound the seek time to be inside the media range.
   NS_ASSERTION(mStartTime != -1, "Should know start time by now");
   NS_ASSERTION(mEndTime != -1, "Should know end time by now");
   mSeekTime = NS_MIN(mSeekTime, mEndTime);
   mSeekTime = NS_MAX(mStartTime, mSeekTime);
+  mBasePosition = mSeekTime;
   LOG(PR_LOG_DEBUG, ("%p Changed state to SEEKING (to %f)", mDecoder.get(), aTime));
   mState = DECODER_STATE_SEEKING;
   if (mDecoder->GetDecodedStream()) {
     mDecoder->RecreateDecodedStream(mSeekTime - mStartTime);
   }
   ScheduleStateMachine();
 }
 
@@ -2008,17 +2042,18 @@ nsresult MediaDecoderStateMachine::RunSt
       // an active decode thread.
       if (NS_FAILED(ScheduleDecodeThread())) {
         NS_WARNING("Failed to start media decode thread!");
         return NS_ERROR_FAILURE;
       }
 
       AdvanceFrame();
       NS_ASSERTION(mDecoder->GetState() != MediaDecoder::PLAY_STATE_PLAYING ||
-                   IsStateMachineScheduled(), "Must have timer scheduled");
+                   IsStateMachineScheduled() ||
+                   mPlaybackRate == 0.0, "Must have timer scheduled");
       return NS_OK;
     }
 
     case DECODER_STATE_BUFFERING: {
       if (IsPausedAndDecoderWaiting()) {
         // The decode buffers are full, and playback is paused. Shutdown the
         // decode thread.
         StopDecodeThread();
@@ -2029,19 +2064,19 @@ nsresult MediaDecoderStateMachine::RunSt
       NS_ASSERTION(!mBufferingStart.IsNull(), "Must know buffering start time.");
 
       // We will remain in the buffering state if we've not decoded enough
       // data to begin playback, or if we've not downloaded a reasonable
       // amount of data inside our buffering time.
       TimeDuration elapsed = now - mBufferingStart;
       bool isLiveStream = mDecoder->GetResource()->GetLength() == -1;
       if ((isLiveStream || !mDecoder->CanPlayThrough()) &&
-            elapsed < TimeDuration::FromSeconds(mBufferingWait) &&
+            elapsed < TimeDuration::FromSeconds(mBufferingWait * mPlaybackRate) &&
             (mQuickBuffering ? HasLowDecodedData(QUICK_BUFFERING_LOW_DATA_USECS)
-                            : (GetUndecodedData() < mBufferingWait * USECS_PER_S)) &&
+                            : (GetUndecodedData() < mBufferingWait * mPlaybackRate * USECS_PER_S)) &&
             !resource->IsDataCachedToEndOfResource(mDecoder->mDecoderPosition) &&
             !resource->IsSuspended())
       {
         LOG(PR_LOG_DEBUG,
             ("%p Buffering: %.3lfs/%ds, timeout in %.3lfs %s",
               mDecoder.get(),
               GetUndecodedData() / static_cast<double>(USECS_PER_S),
               mBufferingWait,
@@ -2090,16 +2125,17 @@ nsresult MediaDecoderStateMachine::RunSt
       // once to ensure the current playback position is advanced to the
       // end of the media, and so that we update the readyState.
       if (mState == DECODER_STATE_COMPLETED &&
           (mReader->VideoQueue().GetSize() > 0 ||
           (HasAudio() && !mAudioCompleted)))
       {
         AdvanceFrame();
         NS_ASSERTION(mDecoder->GetState() != MediaDecoder::PLAY_STATE_PLAYING ||
+                     mPlaybackRate == 0 ||
                      IsStateMachineScheduled(),
                      "Must have timer scheduled");
         return NS_OK;
       }
 
       // StopPlayback in order to reset the IsPlaying() state so audio
       // is restarted correctly.
       StopPlayback();
@@ -2160,26 +2196,42 @@ MediaDecoderStateMachine::GetAudioClock(
   if (!mAudioStream) {
     // Audio thread hasn't played any data yet.
     return mAudioStartTime;
   }
   int64_t t = mAudioStream->GetPosition();
   return (t == -1) ? -1 : t + mAudioStartTime;
 }
 
-void MediaDecoderStateMachine::AdvanceFrame()
+int64_t MediaDecoderStateMachine::GetVideoStreamPosition()
 {
+  mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
+
+  if (!IsPlaying()) {
+    return mPlayDuration + mStartTime;
+  }
+
+  // The playbackRate has been just been changed, reset the playstartTime.
+  if (mResetPlayStartTime) {
+    mPlayStartTime = TimeStamp::Now();
+    mResetPlayStartTime = false;
+  }
+
+  int64_t pos = DurationToUsecs(TimeStamp::Now() - mPlayStartTime) + mPlayDuration;
+  pos -= mBasePosition;
+  if (pos >= 0) {
+    int64_t final = mBasePosition + pos * mPlaybackRate + mStartTime;
+    return final;
+  }
+  return mPlayDuration + mStartTime;
+}
+
+int64_t MediaDecoderStateMachine::GetClock() {
   NS_ASSERTION(OnStateMachineThread(), "Should be on state machine thread.");
   mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
-  NS_ASSERTION(!HasAudio() || mAudioStartTime != -1,
-               "Should know audio start time if we have audio.");
-
-  if (mDecoder->GetState() != MediaDecoder::PLAY_STATE_PLAYING) {
-    return;
-  }
 
   // Determine the clock time. If we've got audio, and we've not reached
   // the end of the audio, use the audio clock. However if we've finished
   // audio, or don't have audio, use the system clock.
   int64_t clock_time = -1;
   if (!IsPlaying()) {
     clock_time = mPlayDuration + mStartTime;
   } else {
@@ -2187,23 +2239,43 @@ void MediaDecoderStateMachine::AdvanceFr
     if (HasAudio() && !mAudioCompleted && audio_time != -1) {
       clock_time = audio_time;
       // Resync against the audio clock, while we're trusting the
       // audio clock. This ensures no "drift", particularly on Linux.
       mPlayDuration = clock_time - mStartTime;
       mPlayStartTime = TimeStamp::Now();
     } else {
       // Audio is disabled on this system. Sync to the system clock.
-      clock_time = DurationToUsecs(TimeStamp::Now() - mPlayStartTime) + mPlayDuration;
+      clock_time = GetVideoStreamPosition();
       // Ensure the clock can never go backwards.
-      NS_ASSERTION(mCurrentFrameTime <= clock_time, "Clock should go forwards");
-      clock_time = NS_MAX(mCurrentFrameTime, clock_time) + mStartTime;
+      NS_ASSERTION(mCurrentFrameTime <= clock_time || mPlaybackRate <= 0,
+          "Clock should go forwards if the playback rate is > 0.");
     }
   }
+  return clock_time;
+}
 
+void MediaDecoderStateMachine::AdvanceFrame()
+{
+  NS_ASSERTION(OnStateMachineThread(), "Should be on state machine thread.");
+  mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
+  NS_ASSERTION(!HasAudio() || mAudioStartTime != -1,
+               "Should know audio start time if we have audio.");
+
+  if (mDecoder->GetState() != MediaDecoder::PLAY_STATE_PLAYING) {
+    return;
+  }
+
+  // If playbackRate is 0.0, we should stop the progress, but not be in paused
+  // state, per spec.
+  if (mPlaybackRate == 0.0) {
+    return;
+  }
+
+  int64_t clock_time = GetClock();
   // Skip frames up to the frame at the playback position, and figure out
   // the time remaining until it's time to display the next frame.
   int64_t remainingTime = AUDIO_DURATION_USECS;
   NS_ASSERTION(clock_time >= mStartTime, "Should have positive clock time.");
   nsAutoPtr<VideoData> currentFrame;
 #ifdef PR_LOGGING
   int32_t droppedFrames = 0;
 #endif
@@ -2226,19 +2298,18 @@ void MediaDecoderStateMachine::AdvanceFr
       mDecoder->UpdatePlaybackOffset(frame->mOffset);
       if (mReader->VideoQueue().GetSize() == 0)
         break;
       frame = mReader->VideoQueue().PeekFront();
     }
     // Current frame has already been presented, wait until it's time to
     // present the next frame.
     if (frame && !currentFrame) {
-      int64_t now = IsPlaying()
-        ? (DurationToUsecs(TimeStamp::Now() - mPlayStartTime) + mPlayDuration)
-        : mPlayDuration;
+      int64_t now = IsPlaying() ? clock_time : mPlayDuration;
+
       remainingTime = frame->mTime - mStartTime - now;
     }
   }
 
   // Check to see if we don't have enough data to play up to the next frame.
   // If we don't, switch to buffering mode.
   MediaResource* resource = mDecoder->GetResource();
   if (mState == DECODER_STATE_DECODING &&
@@ -2276,18 +2347,17 @@ void MediaDecoderStateMachine::AdvanceFr
     // If we're no longer playing after dropping and reacquiring the lock,
     // playback must've been stopped on the decode thread (by a seek, for
     // example).  In that case, the current frame is probably out of date.
     if (!IsPlaying()) {
       ScheduleStateMachine();
       return;
     }
     mDecoder->GetFrameStatistics().NotifyPresentedFrame();
-    int64_t now = DurationToUsecs(TimeStamp::Now() - mPlayStartTime) + mPlayDuration;
-    remainingTime = currentFrame->mEndTime - mStartTime - now;
+    remainingTime = currentFrame->mEndTime - mStartTime - clock_time;
     currentFrame = nullptr;
   }
 
   // Cap the current time to the larger of the audio and video end time.
   // This ensures that if we're running off the system clock, we don't
   // advance the clock to after the media end time.
   if (mVideoFrameEndTime != -1 || mAudioEndTime != -1) {
     // These will be non -1 if we've displayed a video frame, or played an audio frame.
@@ -2584,16 +2654,51 @@ nsIThread* MediaDecoderStateMachine::Get
 }
 
 void MediaDecoderStateMachine::NotifyAudioAvailableListener()
 {
   mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
   mEventManager.NotifyAudioAvailableListener();
 }
 
+void MediaDecoderStateMachine::SetPlaybackRate(double aPlaybackRate)
+{
+  NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
+  NS_ASSERTION(aPlaybackRate != 0,
+      "PlaybackRate == 0 should be handled before this function.");
+  ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
+
+  if (mPlaybackRate == aPlaybackRate) {
+    return;
+  }
+
+  // Get position of the last time we changed the rate.
+  if (!HasAudio()) {
+    // mBasePosition is a position in the video stream, not an absolute time.
+    mBasePosition = GetVideoStreamPosition();
+    if (IsPlaying()) {
+      mPlayDuration = mBasePosition - mStartTime;
+      mResetPlayStartTime = true;
+      mPlayStartTime = TimeStamp::Now();
+    }
+  }
+
+  mPlaybackRate = aPlaybackRate;
+}
+
+void MediaDecoderStateMachine::SetPreservesPitch(bool aPreservesPitch)
+{
+  NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
+  ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
+
+  mPreservesPitch = aPreservesPitch;
+
+  return;
+}
+
 bool MediaDecoderStateMachine::IsShutdown()
 {
   mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
   return GetState() == DECODER_STATE_SHUTDOWN;
 }
 
 } // namespace mozilla
 
--- a/content/media/MediaDecoderStateMachine.h
+++ b/content/media/MediaDecoderStateMachine.h
@@ -228,16 +228,19 @@ public:
   bool IsSeeking() const {
     mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
 
     return mState == DECODER_STATE_SEEKING;
   }
 
   nsresult GetBuffered(nsTimeRanges* aBuffered);
 
+  void SetPlaybackRate(double aPlaybackRate);
+  void SetPreservesPitch(bool aPreservesPitch);
+
   int64_t VideoQueueMemoryInUse() {
     if (mReader) {
       return mReader->VideoQueueMemoryInUse();
     }
     return 0;
   }
 
   int64_t AudioQueueMemoryInUse() {
@@ -395,16 +398,26 @@ private:
 
   // Resets playback timing data. Called when we seek, on the decode thread.
   void ResetPlayback();
 
   // Returns the audio clock, if we have audio, or -1 if we don't.
   // Called on the state machine thread.
   int64_t GetAudioClock();
 
+  // Get the video stream position, taking the |playbackRate| change into
+  // account. This is a position in the media, not the duration of the playback
+  // so far.
+  int64_t GetVideoStreamPosition();
+
+  // Return the current time, either the audio clock if available (if the media
+  // has audio, and the playback is possible), or a clock for the video.
+  // Called on the state machine thread.
+  int64_t GetClock();
+
   // Returns the presentation time of the first audio or video frame in the
   // media.  If the media has video, it returns the first video frame. The
   // decoder monitor must be held with exactly one lock count. Called on the
   // state machine thread.
   VideoData* FindStartTime();
 
   // Update only the state machine's current playback position (and duration,
   // if unknown).  Does not update the playback position on the decoder or
@@ -459,17 +472,17 @@ private:
   nsresult StartAudioThread();
 
   // The main loop for the audio thread. Sent to the thread as
   // an nsRunnableMethod. This continually does blocking writes to
   // to audio stream to play audio data.
   void AudioLoop();
 
   // Sets internal state which causes playback of media to pause.
-  // The decoder monitor must be held. Called on the main, state machine,
+  // The decoder monitor must be held. Called on the state machine,
   // and decode threads.
   void StopPlayback();
 
   // Sets internal state which causes playback of media to begin or resume.
   // Must be called with the decode monitor held. Called on the state machine
   // and decode threads.
   void StartPlayback();
 
@@ -572,16 +585,21 @@ private:
   // or via an event. Access protected by decoder monitor.
   TimeStamp mTimeout;
 
   // The time that playback started from the system clock. This is used for
   // timing the presentation of video frames when there's no audio.
   // Accessed only via the state machine thread.
   TimeStamp mPlayStartTime;
 
+  // When the playbackRate changes, and there is no audio clock, it is necessary
+  // to reset the mPlayStartTime. This is done next time the clock is queried,
+  // when this member is true. Access protected by decoder monitor.
+  bool mResetPlayStartTime;
+
   // The amount of time we've spent playing already the media. The current
   // playback position is therefore |Now() - mPlayStartTime +
   // mPlayDuration|, which must be adjusted by mStartTime if used with media
   // timestamps.  Accessed only via the state machine thread.
   int64_t mPlayDuration;
 
   // Time that buffering started. Used for buffering timeout and only
   // accessed on the state machine thread. This is null while we're not
@@ -646,16 +664,28 @@ private:
   // in microseconds. Accessed from the state machine thread.
   int64_t mVideoFrameEndTime;
 
   // Volume of playback. 0.0 = muted. 1.0 = full volume. Read/Written
   // from the state machine and main threads. Synchronised via decoder
   // monitor.
   double mVolume;
 
+  // Playback rate. 1.0 : normal speed, 0.5 : two times slower. Synchronized via
+  // decoder monitor.
+  double mPlaybackRate;
+
+  // Pitch preservation for the playback rate. Synchronized via decoder monitor.
+  bool mPreservesPitch;
+
+  // Position at which the last playback rate change occured, used to compute
+  // the actual position in the stream when the playback rate changes and there
+  // is no audio to be sync-ed to. Synchronized via decoder monitor.
+  int64_t mBasePosition;
+
   // Time at which we started decoding. Synchronised via decoder monitor.
   TimeStamp mDecodeStartTime;
 
   // The maximum number of second we spend buffering when we are short on
   // unbuffered data.
   uint32_t mBufferingWait;
   int64_t  mLowDataThresholdUsecs;
 
--- a/dom/interfaces/html/nsIDOMHTMLAudioElement.idl
+++ b/dom/interfaces/html/nsIDOMHTMLAudioElement.idl
@@ -15,17 +15,17 @@
  * <audio> element.
  *
  * For more information on this interface, please see
  * http://www.whatwg.org/specs/web-apps/current-work/#audio
  *
  * @status UNDER_DEVELOPMENT
  */
 
-[scriptable, uuid(771a0d40-18ed-11e2-89ca-10bf48d64bd4)]
+[scriptable, uuid(5215662f-9ab5-4219-adb1-672b870b08ba)]
 interface nsIDOMHTMLAudioElement : nsIDOMHTMLMediaElement
 {
   // Setup the audio stream for writing
   void mozSetup(in uint32_t channels, in uint32_t rate);
 
   // Write audio to the audio stream
   [implicit_jscontext]
   unsigned long mozWriteAudio(in jsval data);
--- a/dom/interfaces/html/nsIDOMHTMLMediaElement.idl
+++ b/dom/interfaces/html/nsIDOMHTMLMediaElement.idl
@@ -22,17 +22,17 @@ interface nsIDOMMediaStream;
 
 // undef the GetCurrentTime macro defined in WinBase.h from the MS Platform SDK
 %{C++
 #ifdef GetCurrentTime
 #undef GetCurrentTime
 #endif
 %}
 
-[scriptable, uuid(4319d74a-ef91-46e0-b6e1-b35036fc4024)]
+[scriptable, uuid(efa52d52-a876-486f-8739-87e1a13bc42d)]
 interface nsIDOMHTMLMediaElement : nsIDOMHTMLElement
 {
   // error state
   readonly attribute nsIDOMMediaError error;
 
   // network state
            attribute DOMString src;
   [implicit_jscontext] attribute jsval mozSrcObject;
@@ -57,16 +57,19 @@ interface nsIDOMHTMLMediaElement : nsIDO
   readonly attribute unsigned short readyState;
   readonly attribute boolean seeking;
 
   // playback state
            attribute double currentTime;
   readonly attribute double initialTime;
   readonly attribute double duration;
   readonly attribute boolean paused;
+           attribute double defaultPlaybackRate;
+           attribute double playbackRate;
+           attribute boolean mozPreservesPitch;
   readonly attribute nsIDOMTimeRanges played;
   readonly attribute nsIDOMTimeRanges seekable;
   readonly attribute boolean ended;
   readonly attribute boolean mozAutoplayEnabled;
            attribute boolean autoplay;
            attribute boolean loop;
   void play();
   void pause();
--- a/dom/interfaces/html/nsIDOMHTMLVideoElement.idl
+++ b/dom/interfaces/html/nsIDOMHTMLVideoElement.idl
@@ -11,17 +11,17 @@
  * <video> element.
  *
  * For more information on this interface, please see
  * http://www.whatwg.org/specs/web-apps/current-work/#video
  *
  * @status UNDER_DEVELOPMENT
  */
 
-[scriptable, uuid(84395418-18ed-11e2-bbda-10bf48d64bd4)]
+[scriptable, uuid(5f33df40-25d7-4d7b-9b2f-0b9acd9e5ad7)]
 interface nsIDOMHTMLVideoElement : nsIDOMHTMLMediaElement
 {
            attribute long width; 
            attribute long height;
   readonly attribute unsigned long videoWidth;
   readonly attribute unsigned long videoHeight;
            attribute DOMString poster;