Bug 576539 - Inject silence when decoder encounters missing audio in WebM and Ogg media. r=kinetik a=blocking2.0
authorChris Pearce <chris@pearce.org.nz>
Fri, 13 Aug 2010 14:28:15 +1200
changeset 50359 94eacfad6472a3cbdc0931b98d4dcc6958d7125d
parent 50358 117166848ffac0fc6a9c1854850e3293c22cdcb2
child 50360 b88ed396410049c590aa32087cb3e5050ea9b19a
push idunknown
push userunknown
push dateunknown
reviewerskinetik, blocking2
bugs576539
milestone2.0b4pre
Bug 576539 - Inject silence when decoder encounters missing audio in WebM and Ogg media. r=kinetik a=blocking2.0
content/media/VideoUtils.cpp
content/media/VideoUtils.h
content/media/nsBuiltinDecoderReader.cpp
content/media/nsBuiltinDecoderStateMachine.cpp
content/media/nsBuiltinDecoderStateMachine.h
content/media/ogg/nsOggReader.cpp
content/media/test/Makefile.in
content/media/test/audio-gaps.ogg
content/media/test/manifest.js
content/media/webm/nsWebMReader.cpp
content/media/webm/nsWebMReader.h
--- a/content/media/VideoUtils.cpp
+++ b/content/media/VideoUtils.cpp
@@ -169,8 +169,30 @@ PRBool MulOverflow(PRInt64 a, PRInt64 b,
   if (!AddOverflow(q, static_cast<PRInt64>(lo), aResult)) {
     return PR_FALSE;
   }
 
   aResult *= sign;
   NS_ASSERTION(a * b == aResult, "We didn't overflow, but result is wrong!");
   return PR_TRUE;
 }
+
+// Converts from number of audio samples to milliseconds, given the specified
+// audio rate.
+PRBool SamplesToMs(PRInt64 aSamples, PRUint32 aRate, PRInt64& aOutMs)
+{
+  PRInt64 x;
+  if (!MulOverflow(aSamples, 1000, x))
+    return PR_FALSE;
+  aOutMs = x / aRate;
+  return PR_TRUE;
+}
+
+// Converts from milliseconds to number of audio samples, given the specified
+// audio rate.
+PRBool MsToSamples(PRInt64 aMs, PRUint32 aRate, PRInt64& aOutSamples)
+{
+  PRInt64 x;
+  if (!MulOverflow(aMs, aRate, x))
+    return PR_FALSE;
+  aOutSamples = x / 1000;
+  return PR_TRUE;
+}
--- a/content/media/VideoUtils.h
+++ b/content/media/VideoUtils.h
@@ -121,9 +121,21 @@ PRBool MulOverflow32(PRUint32 a, PRUint3
 // if addition would result in an overflow.
 PRBool AddOverflow(PRInt64 a, PRInt64 b, PRInt64& aResult);
 
 // 64 bit integer multiplication with overflow checking. Returns PR_TRUE
 // if the multiplication was successful, or PR_FALSE if the operation resulted
 // in an integer overflow.
 PRBool MulOverflow(PRInt64 a, PRInt64 b, PRInt64& aResult);
 
+// Converts from number of audio samples (aSamples) to milliseconds, given
+// the specified audio rate (aRate). Stores result in aOutMs. Returns PR_TRUE
+// if the operation succeeded, or PR_FALSE if there was an integer overflow
+// while calulating the conversion.
+PRBool SamplesToMs(PRInt64 aSamples, PRUint32 aRate, PRInt64& aOutMs);
+
+// Converts from milliseconds (aMs) to number of audio samples, given the
+// specified audio rate (aRate). Stores the result in aOutSamples. Returns
+// PR_TRUE if the operation succeeded, or PR_FALSE if there was an integer
+// overflow while calulating the conversion.
+PRBool MsToSamples(PRInt64 aMs, PRUint32 aRate, PRInt64& aOutSamples);
+
 #endif
--- a/content/media/nsBuiltinDecoderReader.cpp
+++ b/content/media/nsBuiltinDecoderReader.cpp
@@ -251,17 +251,17 @@ nsBuiltinDecoderReader::GetSeekRange(con
       // Target lies exactly in this range.
       return ranges[i];
     }
   }
   return aExact ? ByteRange() : ByteRange(so, eo, st, et);
 }
 
 VideoData* nsBuiltinDecoderReader::FindStartTime(PRInt64 aOffset,
-                                      PRInt64& aOutStartTime)
+                                                 PRInt64& aOutStartTime)
 {
   NS_ASSERTION(mDecoder->OnStateMachineThread(), "Should be on state machine thread.");
 
   if (NS_FAILED(ResetDecode())) {
     return nsnull;
   }
 
   // Extract the start times of the bitstreams in order to calculate
--- a/content/media/nsBuiltinDecoderStateMachine.cpp
+++ b/content/media/nsBuiltinDecoderStateMachine.cpp
@@ -77,16 +77,23 @@ extern PRLogModuleInfo* gBuiltinDecoderL
 // less than LOW_AUDIO_MS of audio, or if we've got video and have queued
 // less than LOW_VIDEO_FRAMES frames.
 static const PRUint32 LOW_AUDIO_MS = 300;
 
 // If more than this many ms of decoded audio is queued, we'll hold off
 // decoding more audio.
 const unsigned AMPLE_AUDIO_MS = 2000;
 
+// Maximum number of bytes we'll allocate and write at once to the audio
+// hardware when the audio stream contains missing samples and we're
+// writing silence in order to fill the gap. We limit our silence-writes
+// to 32KB in order to avoid allocating an impossibly large chunk of
+// memory if we encounter a large chunk of silence.
+const PRUint32 SILENCE_BYTES_CHUNK = 32 * 1024;
+
 // If we have fewer than LOW_VIDEO_FRAMES decoded frames, and
 // we're not "pumping video", we'll skip the video up to the next keyframe
 // which is at or after the current playback position.
 //
 // Also if the decode catches up with the end of the downloaded data,
 // we'll only go into BUFFERING state if we've got audio and have queued
 // less than LOW_AUDIO_MS of audio, or if we've got video and have queued
 // less than LOW_VIDEO_FRAMES frames.
@@ -321,21 +328,27 @@ PRBool nsBuiltinDecoderStateMachine::IsP
 
   return !mPlayStartTime.IsNull();
 }
 
 void nsBuiltinDecoderStateMachine::AudioLoop()
 {
   NS_ASSERTION(OnAudioThread(), "Should be on audio thread.");
   LOG(PR_LOG_DEBUG, ("Begun audio thread/loop"));
+  PRUint64 audioDuration = 0;
+  PRInt64 audioStartTime = -1;
+  PRUint32 channels, rate;
   {
     MonitorAutoEnter mon(mDecoder->GetMonitor());
     mAudioCompleted = PR_FALSE;
+    audioStartTime = mAudioStartTime;
+    channels = mReader->GetInfo().mAudioChannels;
+    rate = mReader->GetInfo().mAudioRate;
+    NS_ASSERTION(audioStartTime != -1, "Should have audio start time by now");
   }
-  PRInt64 audioStartTime = -1;
   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.
     {
       MonitorAutoEnter mon(mDecoder->GetMonitor());
       NS_ASSERTION(mState != DECODER_STATE_DECODING_METADATA,
                    "Should have meta data before audio started playing.");
@@ -355,64 +368,67 @@ void nsBuiltinDecoderStateMachine::Audio
           mReader->mAudioQueue.AtEndOfStream())
       {
         break;
       }
     }
 
     NS_ASSERTION(mReader->mAudioQueue.GetSize() > 0,
                  "Should have data to play");
-    nsAutoPtr<SoundData> sound(mReader->mAudioQueue.PopFront());
-    {
-      MonitorAutoEnter mon(mDecoder->GetMonitor());
-      NS_WARN_IF_FALSE(IsPlaying(), "Should be playing");
-      // Awaken the decode loop if it's waiting for space to free up in the
-      // audio queue.
-      mDecoder->GetMonitor().NotifyAll();
+    // See if there's missing samples in the audio stream. If there is, push
+    // silence into the audio hardware, so we can play across the gap.
+    const SoundData* s = mReader->mAudioQueue.PeekFront();
+
+    // Calculate the number of samples that have been pushed onto the audio
+    // hardware.
+    PRInt64 playedSamples = 0;
+    if (!MsToSamples(audioStartTime, rate, playedSamples)) {
+      NS_WARNING("Int overflow converting playedSamples");
+      break;
     }
-
-    if (audioStartTime == -1) {
-      // Remember the presentation time of the first audio sample we play.
-      // We add this to the position/played duration of the audio stream to
-      // determine the audio clock time. Used for A/V sync.
-      MonitorAutoEnter mon(mDecoder->GetMonitor());
-      mAudioStartTime = audioStartTime = sound->mTime;
-      LOG(PR_LOG_DEBUG, ("First audio sample has timestamp %lldms", mAudioStartTime));
+    if (!AddOverflow(playedSamples, audioDuration, playedSamples)) {
+      NS_WARNING("Int overflow adding playedSamples");
+      break;
     }
 
-    PRInt64 audioEndTime = -1;
-    {
-      MonitorAutoEnter audioMon(mAudioMonitor);
-      if (mAudioStream) {
-        // The state machine could have paused since we've released the decoder
-        // monitor and acquired the audio monitor. Rather than acquire both
-        // monitors, the audio stream also maintains whether its paused or not.
-        // This prevents us from doing a blocking write while holding the audio
-        // monitor while paused; we would block, and the state machine won't be 
-        // able to acquire the audio monitor in order to resume or destroy the
-        // audio stream.
-        if (!mAudioStream->IsPaused()) {
-          mAudioStream->Write(sound->mAudioData,
-                              sound->AudioDataLength(),
-                              PR_TRUE);
-          audioEndTime = sound->mTime + sound->mDuration;
-          mDecoder->UpdatePlaybackOffset(sound->mOffset);
-        } else {
-          mReader->mAudioQueue.PushFront(sound);
-          sound.forget();
-        }
-      }
+    // Calculate the timestamp of the next chunk of audio in numbers of
+    // samples.
+    PRInt64 sampleTime = 0;
+    if (!MsToSamples(s->mTime, rate, sampleTime)) {
+      NS_WARNING("Int overflow converting sampleTime");
+      break;
+    }
+    PRInt64 missingSamples = 0;
+    if (!AddOverflow(sampleTime, -playedSamples, missingSamples)) {
+      NS_WARNING("Int overflow adding missingSamples");
+      break;
     }
-    sound = nsnull;
 
+    if (missingSamples > 0) {
+      // The next sound chunk begins some time after the end of the last chunk
+      // we pushed to the sound hardware. We must push silence into the audio
+      // hardware so that the next sound chunk begins playback at the correct
+      // time.
+      missingSamples = NS_MIN(static_cast<PRInt64>(PR_UINT32_MAX), missingSamples);
+      audioDuration += PlaySilence(static_cast<PRUint32>(missingSamples), channels);
+    } else {
+      audioDuration += PlayFromAudioQueue();
+    }
     {
       MonitorAutoEnter mon(mDecoder->GetMonitor());
-      if (audioEndTime != -1) {
-        mAudioEndTime = audioEndTime;
+      PRInt64 playedMs;
+      if (!SamplesToMs(audioDuration, rate, playedMs)) {
+        NS_WARNING("Int overflow calculating playedMs");
+        break;
       }
+      if (!AddOverflow(audioStartTime, playedMs, mAudioEndTime)) {
+        NS_WARNING("Int overflow calculating audio end time");
+        break;
+      }
+
       PRInt64 audioAhead = mAudioEndTime - mCurrentFrameTime - mStartTime;
       if (audioAhead > AMPLE_AUDIO_MS) {
         // We've pushed enough audio onto the hardware that we've queued up a
         // significant amount ahead of the playback position. The decode
         // thread will be going to sleep, so we won't get any new samples
         // anyway, so sleep until we need to push to the hardware again.
         Wait(AMPLE_AUDIO_MS / 2);
         // Kick the decode thread; since above we only do a NotifyAll when
@@ -443,16 +459,75 @@ void nsBuiltinDecoderStateMachine::Audio
     UpdateReadyState();
     // Kick the decode and state machine threads; they may be sleeping waiting
     // for this to finish.
     mDecoder->GetMonitor().NotifyAll();
   }
   LOG(PR_LOG_DEBUG, ("Audio stream finished playing, audio thread exit"));
 }
 
+PRUint32 nsBuiltinDecoderStateMachine::PlaySilence(PRUint32 aSamples, PRUint32 aChannels)
+{
+  MonitorAutoEnter audioMon(mAudioMonitor);
+  if (mAudioStream->IsPaused()) {
+    // The state machine has paused since we've released the decoder
+    // monitor and acquired the audio monitor. Don't write any audio.
+    return 0;
+  }
+  PRUint32 maxSamples = SILENCE_BYTES_CHUNK / aChannels;
+  PRUint32 samples = NS_MIN(aSamples, maxSamples);
+  PRUint32 numFloats = samples * aChannels;
+  nsAutoArrayPtr<float> buf(new float[numFloats]);
+  memset(buf.get(), 0, sizeof(float) * numFloats);
+  mAudioStream->Write(buf, numFloats, PR_TRUE);
+  return samples;
+}
+
+PRUint32 nsBuiltinDecoderStateMachine::PlayFromAudioQueue()
+{
+  nsAutoPtr<SoundData> sound(mReader->mAudioQueue.PopFront());
+  {
+    MonitorAutoEnter mon(mDecoder->GetMonitor());
+    NS_WARN_IF_FALSE(IsPlaying(), "Should be playing");
+    // Awaken the decode loop if it's waiting for space to free up in the
+    // audio queue.
+    mDecoder->GetMonitor().NotifyAll();
+  }
+  PRInt64 offset = -1;
+  PRUint32 samples = 0;
+  {
+    MonitorAutoEnter audioMon(mAudioMonitor);
+    if (!mAudioStream) {
+      return 0;
+    }
+    // The state machine could have paused since we've released the decoder
+    // monitor and acquired the audio monitor. Rather than acquire both
+    // monitors, the audio stream also maintains whether its paused or not.
+    // This prevents us from doing a blocking write while holding the audio
+    // monitor while paused; we would block, and the state machine won't be 
+    // able to acquire the audio monitor in order to resume or destroy the
+    // audio stream.
+    if (!mAudioStream->IsPaused()) {
+      mAudioStream->Write(sound->mAudioData,
+                          sound->AudioDataLength(),
+                          PR_TRUE);
+      offset = sound->mOffset;
+      samples = sound->mSamples;
+    } else {
+      mReader->mAudioQueue.PushFront(sound);
+      sound.forget();
+    }
+  }
+  if (offset != -1) {
+    mDecoder->UpdatePlaybackOffset(offset);
+  }
+  return samples;
+}
+
+
 nsresult nsBuiltinDecoderStateMachine::Init()
 {
   return mReader->Init();
 }
 
 void nsBuiltinDecoderStateMachine::StopPlayback(eStopMode aMode)
 {
   NS_ASSERTION(IsCurrentThread(mDecoder->mStateMachineThread),
@@ -860,20 +935,23 @@ nsresult nsBuiltinDecoderStateMachine::R
           nsresult res;
           {
             MonitorAutoExit exitMon(mDecoder->GetMonitor());
             // Now perform the seek. We must not hold the state machine monitor
             // while we seek, since the seek decodes.
             res = mReader->Seek(seekTime, mStartTime, mEndTime);
           }
           if (NS_SUCCEEDED(res)){
+            PRInt64 audioTime = seekTime;
             SoundData* audio = HasAudio() ? mReader->mAudioQueue.PeekFront() : nsnull;
             if (audio) {
-              mPlayDuration = TimeDuration::FromMilliseconds(audio->mTime);
+              audioTime = audio->mTime;
+              mPlayDuration = TimeDuration::FromMilliseconds(mAudioStartTime);
             }
+            mAudioStartTime = (audioTime >= seekTime) ? seekTime : audioTime;
             if (HasVideo()) {
               nsAutoPtr<VideoData> video(mReader->mVideoQueue.PeekFront());
               if (video) {
                 RenderVideoFrame(video);
                 if (!audio) {
                   NS_ASSERTION(video->mTime <= seekTime &&
                                seekTime <= video->mEndTime,
                                "Seek target should lie inside the first frame after seek");
@@ -1176,16 +1254,20 @@ VideoData* nsBuiltinDecoderStateMachine:
       NS_ASSERTION(mEndTime != -1,
                    "We should have mEndTime as supplied duration here");
       // We were specified a duration from a Content-Duration HTTP header.
       // Adjust mEndTime so that mEndTime-mStartTime matches the specified
       // duration.
       mEndTime = mStartTime + mEndTime;
     }
   }
+  // Set the audio start time to be start of media. If this lies before the
+  // first acutal audio sample we have, we'll inject silence during playback
+  // to ensure the audio starts at the correct time.
+  mAudioStartTime = mStartTime;
   LOG(PR_LOG_DEBUG, ("%p Media start time is %lldms", mDecoder, mStartTime));
   return v;
 }
 
 void nsBuiltinDecoderStateMachine::FindEndTime() 
 {
   NS_ASSERTION(OnStateMachineThread(), "Should be on state machine thread.");
   mDecoder->GetMonitor().AssertCurrentThreadIn();
--- a/content/media/nsBuiltinDecoderStateMachine.h
+++ b/content/media/nsBuiltinDecoderStateMachine.h
@@ -281,16 +281,29 @@ protected:
 
   // If we have video, display a video frame if it's time for display has
   // arrived, otherwise sleep until it's time for the next sample. Update
   // the current frame time as appropriate, and trigger ready state update.
   // The decoder monitor must be held with exactly one lock count. Called
   // on the state machine thread.
   void AdvanceFrame();
 
+  // Pushes up to aSamples samples of silence onto the audio hardware. Returns
+  // the number of samples acutally pushed to the hardware. This pushes up to
+  // 32KB worth of samples to the hardware before returning, so must be called
+  // in a loop to ensure that the desired number of samples are pushed to the
+  // hardware. This ensures that the playback position advances smoothly, and
+  // guarantees that we don't try to allocate an impossibly large chunk of
+  // memory in order to play back silence. Called on the audio thread.
+  PRUint32 PlaySilence(PRUint32 aSamples, PRUint32 aChannels);
+
+  // Pops an audio chunk from the front of the audio queue, and pushes its
+  // sound data to the audio hardware. Called on the audio thread.
+  PRUint32 PlayFromAudioQueue();
+
   // Stops the decode threads. The decoder monitor must be held with exactly
   // one lock count. Called on the state machine thread.
   void StopDecodeThreads();
 
   // Starts the decode threads. The decoder monitor must be held with exactly
   // one lock count. Called on the state machine thread.
   nsresult StartDecodeThreads();
 
--- a/content/media/ogg/nsOggReader.cpp
+++ b/content/media/ogg/nsOggReader.cpp
@@ -290,42 +290,40 @@ nsresult nsOggReader::DecodeVorbis(nsTAr
   }
   if (vorbis_synthesis_blockin(&mVorbisState->mDsp,
                                &mVorbisState->mBlock) != 0)
   {
     return NS_ERROR_FAILURE;
   }
 
   float** pcm = 0;
-  PRUint32 samples = 0;
+  PRInt32 samples = 0;
   PRUint32 channels = mVorbisState->mInfo.channels;
   while ((samples = vorbis_synthesis_pcmout(&mVorbisState->mDsp, &pcm)) > 0) {
-    if (samples > 0) {
-      float* buffer = new float[samples * channels];
-      float* p = buffer;
-      for (PRUint32 i = 0; i < samples; ++i) {
-        for (PRUint32 j = 0; j < channels; ++j) {
-          *p++ = pcm[j][i];
-        }
+    float* buffer = new float[samples * channels];
+    float* p = buffer;
+    for (PRUint32 i = 0; i < samples; ++i) {
+      for (PRUint32 j = 0; j < channels; ++j) {
+        *p++ = pcm[j][i];
       }
+    }
 
-      PRInt64 duration = mVorbisState->Time((PRInt64)samples);
-      PRInt64 startTime = (mVorbisGranulepos != -1) ?
-        mVorbisState->Time(mVorbisGranulepos) : -1;
-      SoundData* s = new SoundData(mPageOffset,
-                                   startTime,
-                                   duration,
-                                   samples,
-                                   buffer,
-                                   channels);
-      if (mVorbisGranulepos != -1) {
-        mVorbisGranulepos += samples;
-      }
-      aChunks.AppendElement(s);
+    PRInt64 duration = mVorbisState->Time((PRInt64)samples);
+    PRInt64 startTime = (mVorbisGranulepos != -1) ?
+      mVorbisState->Time(mVorbisGranulepos) : -1;
+    SoundData* s = new SoundData(mPageOffset,
+                                 startTime,
+                                 duration,
+                                 samples,
+                                 buffer,
+                                 channels);
+    if (mVorbisGranulepos != -1) {
+      mVorbisGranulepos += samples;
     }
+    aChunks.AppendElement(s);
     if (vorbis_synthesis_read(&mVorbisState->mDsp, samples) != 0) {
       return NS_ERROR_FAILURE;
     }
   }
   return NS_OK;
 }
 
 PRBool nsOggReader::DecodeAudioData()
--- a/content/media/test/Makefile.in
+++ b/content/media/test/Makefile.in
@@ -162,16 +162,17 @@ ifneq ($(OS_ARCH),WINNT)
 endif
 endif
 
 # sample files
 _TEST_FILES += \
 		320x240.ogv \
 		448636.ogv \
 		audio-overhang.ogg \
+		audio-gaps.ogg \
 		beta-phrasebook.ogg \
 		bogus.ogv \
 		bug495129.ogv \
 		bug495794.ogg \
 		bug461281.ogg \
 		bug482461.ogv \
 		bug498380.ogv \
 		bug498855-1.ogv \
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ce96748ccd7171e1814ed571ce908f5e6a122779
GIT binary patch
literal 12306
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
ITwV9S0ODYA-~a#s
--- a/content/media/test/manifest.js
+++ b/content/media/test/manifest.js
@@ -92,16 +92,19 @@ var gPlayTests = [
   { name:"bug495129.ogv", type:"video/ogg", duration:2.41 },
   
   { name:"bug498380.ogv", type:"video/ogg", duration:0.533 },
   { name:"bug495794.ogg", type:"audio/ogg", duration:0.3 },
   { name:"bug557094.ogv", type:"video/ogg", duration:0.24 },
   { name:"audio-overhang.ogg", type:"audio/ogg", duration:2.3 },
   { name:"video-overhang.ogg", type:"audio/ogg", duration:3.966 },
 
+  // bug461281.ogg with the middle second chopped out.
+  { name:"audio-gaps.ogg", type:"audio/ogg", duration:2.208 },
+
   // Test playback/metadata work after a redirect
   { name:"redirect.sjs?domain=mochi.test:8888&file=320x240.ogv",
     type:"video/ogg", duration:0.233 },
 
   // Test playback of a webm file
   { name:"seek.webm", type:"video/webm", duration:3.966 },
 
   // Test playback of a raw file
--- a/content/media/webm/nsWebMReader.cpp
+++ b/content/media/webm/nsWebMReader.cpp
@@ -113,16 +113,18 @@ static int64_t webm_tell(void *aUserData
 
 nsWebMReader::nsWebMReader(nsBuiltinDecoder* aDecoder)
   : nsBuiltinDecoderReader(aDecoder),
   mContext(nsnull),
   mPacketCount(0),
   mChannels(0),
   mVideoTrack(0),
   mAudioTrack(0),
+  mAudioSamples(0),
+  mAudioStartMs(-1),
   mHasVideo(PR_FALSE),
   mHasAudio(PR_FALSE)
 {
   MOZ_COUNT_CTOR(nsWebMReader);
 }
 
 nsWebMReader::~nsWebMReader()
 {
@@ -150,16 +152,18 @@ nsresult nsWebMReader::Init()
   memset(&mVorbisDsp, 0, sizeof(vorbis_dsp_state));
   memset(&mVorbisBlock, 0, sizeof(vorbis_block));
 
   return NS_OK;
 }
 
 nsresult nsWebMReader::ResetDecode()
 {
+  mAudioSamples = 0;
+  mAudioStartMs = -1;
   nsresult res = NS_OK;
   if (NS_FAILED(nsBuiltinDecoderReader::ResetDecode())) {
     res = NS_ERROR_FAILURE;
   }
 
   // Ignore failed results from vorbis_synthesis_restart. They
   // aren't fatal and it fails when ResetDecode is called at a
   // time when no vorbis data has been read.
@@ -342,17 +346,53 @@ PRBool nsWebMReader::DecodeAudioPacket(n
 
   uint64_t tstamp = 0;
   r = nestegg_packet_tstamp(aPacket, &tstamp);
   if (r == -1) {
     nestegg_free_packet(aPacket);
     return PR_FALSE;
   }
 
+  const PRUint32 rate = mVorbisDsp.vi->rate;
   PRUint64 tstamp_ms = tstamp / NS_PER_MS;
+  if (mAudioStartMs == -1) {
+    // This is the first audio chunk. Assume the start time of our decode
+    // is the start of this chunk.
+    mAudioStartMs = tstamp_ms;
+  }
+  // If there's a gap between the start of this sound chunk and the end of
+  // the previous sound chunk, we need to increment the packet count so that
+  // the vorbis decode doesn't use data from before the gap to help decode
+  // from after the gap.
+  PRInt64 tstamp_samples = 0;
+  if (!MsToSamples(tstamp_ms, rate, tstamp_samples)) {
+    NS_WARNING("Int overflow converting WebM timestamp to samples");
+    return PR_FALSE;
+  }
+  PRInt64 decoded_samples = 0;
+  if (!MsToSamples(mAudioStartMs, rate, decoded_samples)) {
+    NS_WARNING("Int overflow converting WebM start time to samples");
+    return PR_FALSE;
+  }
+  if (!AddOverflow(decoded_samples, mAudioSamples, decoded_samples)) {
+    NS_WARNING("Int overflow adding decoded_samples");
+    return PR_FALSE;
+  }
+  if (tstamp_samples > decoded_samples) {
+#ifdef DEBUG
+    PRInt64 ms = 0;
+    LOG(PR_LOG_DEBUG, ("WebMReader detected gap of %lldms, %lld samples, in audio stream\n",
+      SamplesToMs(tstamp_samples - decoded_samples, rate, ms) ? ms: -1,
+      tstamp_samples - decoded_samples));
+#endif
+    mPacketCount++;
+    mAudioStartMs = tstamp_ms;
+    mAudioSamples = 0;
+  }
+
   for (PRUint32 i = 0; i < count; ++i) {
     unsigned char* data;
     size_t length;
     r = nestegg_packet_data(aPacket, i, &data, &length);
     if (r == -1) {
       nestegg_free_packet(aPacket);
       return PR_FALSE;
     }
@@ -366,37 +406,50 @@ PRBool nsWebMReader::DecodeAudioPacket(n
 
     if (vorbis_synthesis_blockin(&mVorbisDsp,
                                  &mVorbisBlock) != 0) {
       nestegg_free_packet(aPacket);
       return PR_FALSE;
     }
 
     float** pcm = 0;
-    PRUint32 samples = 0;
+    PRInt32 samples = 0;
+    PRInt32 total_samples = 0;
     while ((samples = vorbis_synthesis_pcmout(&mVorbisDsp, &pcm)) > 0) {
-      if (samples > 0) {
-        float* buffer = new float[samples * mChannels];
-        float* p = buffer;
-        for (PRUint32 i = 0; i < samples; ++i) {
-          for (PRUint32 j = 0; j < mChannels; ++j) {
-            *p++ = pcm[j][i];
-          }
+      float* buffer = new float[samples * mChannels];
+      float* p = buffer;
+      for (PRUint32 i = 0; i < samples; ++i) {
+        for (PRUint32 j = 0; j < mChannels; ++j) {
+          *p++ = pcm[j][i];
         }
+      }
 
-        PRInt64 duration = samples * 1000 / mVorbisDsp.vi->rate;
-        SoundData* s = new SoundData(0,
-                                     tstamp_ms,
-                                     duration,
-                                     samples,
-                                     buffer,
-                                     mChannels);
-        mAudioQueue.Push(s);
-        tstamp_ms += duration;
+      PRInt64 duration = 0;
+      if (!SamplesToMs(samples, rate, duration)) {
+        NS_WARNING("Int overflow converting WebM audio duration");
+        nestegg_free_packet(aPacket);
+        return PR_FALSE;
+      }
+      PRInt64 total_duration = 0;
+      if (!SamplesToMs(total_samples, rate, total_duration)) {
+        NS_WARNING("Int overflow converting WebM audio total_duration");
+        nestegg_free_packet(aPacket);
+        return PR_FALSE;
       }
+      
+      PRInt64 time = tstamp_ms + total_duration;
+      total_samples += samples;
+      SoundData* s = new SoundData(0,
+                                   time,
+                                   duration,
+                                   samples,
+                                   buffer,
+                                   mChannels);
+      mAudioQueue.Push(s);
+      mAudioSamples += samples;
       if (vorbis_synthesis_read(&mVorbisDsp, samples) != 0) {
         nestegg_free_packet(aPacket);
         return PR_FALSE;
       }
     }
   }
 
   nestegg_free_packet(aPacket);
--- a/content/media/webm/nsWebMReader.h
+++ b/content/media/webm/nsWebMReader.h
@@ -179,14 +179,20 @@ private:
   // must only be accessed from the state machine thread.
   PacketQueue mVideoPackets;
   PacketQueue mAudioPackets;
 
   // Index of video and audio track to play
   PRUint32 mVideoTrack;
   PRUint32 mAudioTrack;
 
+  // Time in ms of the start of the first audio sample we've decoded.
+  PRInt64 mAudioStartMs;
+
+  // Number of samples we've decoded since decoding began at mAudioStartMs.
+  PRUint64 mAudioSamples;
+
   // Booleans to indicate if we have audio and/or video data
   PRPackedBool mHasVideo;
   PRPackedBool mHasAudio;
 };
 
 #endif