MediaRecorder: support sampling rate adaption in AudioTrackRecorder
This CL adds support for audio coming from the MediaStream
Tracks with a sampling rate not directly supported by Opus
({48k, 24k, 16k, 12k, 8k}samples/s) by adding a AudioConverter
in AudioTrackRecorder::AudioEncoder.
BUG=569089
TEST =
- unit tests, which are beefed up
- content_browsertests
- demo [1]
- corrected bug's file [2]
(correction being: s/opus/webm/ for the mimeType to
use for capture.)
I used a variety of input wav's, mostly the original
sfx5.wav in the bug and the "miscellaneous" entries in [3]
[1] https://webrtc.github.io/samples/src/content/getusermedia/record/
[2] https://rawgit.com/Miguelao/demos/master/audiotranscode.html
[3] http://download.wavetlan.com/SVV/Media/HTTP/http-wav.htm
Review URL: https://codereview.chromium.org/1579693006
Cr-Commit-Position: refs/heads/master@{#372491}
diff --git a/content/renderer/media/audio_track_recorder.cc b/content/renderer/media/audio_track_recorder.cc
index 05107b1..dea0c3d 100644
--- a/content/renderer/media/audio_track_recorder.cc
+++ b/content/renderer/media/audio_track_recorder.cc
@@ -12,38 +12,93 @@
#include "base/stl_util.h"
#include "media/audio/audio_parameters.h"
#include "media/base/audio_bus.h"
+#include "media/base/audio_converter.h"
+#include "media/base/audio_fifo.h"
#include "media/base/bind_to_current_loop.h"
#include "third_party/opus/src/include/opus.h"
// Note that this code follows the Chrome media convention of defining a "frame"
-// as "one multi-channel sample" as opposed to another common definition
-// meaning "a chunk of samples". Here this second definition of "frame" is
-// called a "buffer"; so what might be called "frame duration" is instead
-// "buffer duration", and so on.
+// as "one multi-channel sample" as opposed to another common definition meaning
+// "a chunk of samples". Here this second definition of "frame" is called a
+// "buffer"; so what might be called "frame duration" is instead "buffer
+// duration", and so on.
namespace content {
namespace {
-enum {
- // This is the recommended value, according to documentation in
+enum : int {
+ // Recommended value for opus_encode_float(), according to documentation in
// third_party/opus/src/include/opus.h, so that the Opus encoder does not
- // degrade the audio due to memory constraints.
- OPUS_MAX_PAYLOAD_SIZE = 4000,
+ // degrade the audio due to memory constraints, and is independent of the
+ // duration of the encoded buffer.
+ kOpusMaxDataBytes = 4000,
- // Support for max sampling rate of 48KHz, 2 channels, 60 ms duration.
- MAX_SAMPLES_PER_BUFFER = 48 * 2 * 60,
+ // Opus preferred sampling rate for encoding. This is also the one WebM likes
+ // to have: https://wiki.xiph.org/MatroskaOpus.
+ kOpusPreferredSamplingRate = 48000,
+
+ // For quality reasons we try to encode 60ms, the maximum Opus buffer.
+ kOpusPreferredBufferDurationMs = 60,
+
+ // Maximum amount of buffers that can be held in the AudioEncoders' AudioFifo.
+ // Recording is not real time, hence a certain buffering is allowed.
+ kMaxNumberOfFifoBuffers = 2,
};
+// The amount of Frames in a 60 ms buffer @ 48000 samples/second.
+const int kOpusPreferredFramesPerBuffer = kOpusPreferredSamplingRate *
+ kOpusPreferredBufferDurationMs /
+ base::Time::kMillisecondsPerSecond;
+
+// Tries to encode |data_in|'s |num_samples| into |data_out|.
+bool DoEncode(OpusEncoder* opus_encoder,
+ float* data_in,
+ int num_samples,
+ std::string* data_out) {
+ DCHECK_EQ(kOpusPreferredFramesPerBuffer, num_samples);
+
+ data_out->resize(kOpusMaxDataBytes);
+ const opus_int32 result = opus_encode_float(
+ opus_encoder, data_in, num_samples,
+ reinterpret_cast<uint8_t*>(string_as_array(data_out)), kOpusMaxDataBytes);
+
+ if (result > 1) {
+ // TODO(ajose): Investigate improving this. http://crbug.com/547918
+ data_out->resize(result);
+ return true;
+ }
+ // If |result| in {0,1}, do nothing; the documentation says that a return
+ // value of zero or one means the packet does not need to be transmitted.
+ // Otherwise, we have an error.
+ DLOG_IF(ERROR, result < 0) << " encode failed: " << opus_strerror(result);
+ return false;
+}
+
+// Interleaves |audio_bus| channels() of floats into a single output linear
+// |buffer|.
+// TODO(mcasas) https://crbug.com/580391 use AudioBus::ToInterleavedFloat().
+void ToInterleaved(media::AudioBus* audio_bus, float* buffer) {
+ for (int ch = 0; ch < audio_bus->channels(); ++ch) {
+ const float* src = audio_bus->channel(ch);
+ const float* const src_end = src + audio_bus->frames();
+ float* dest = buffer + ch;
+ for (; src < src_end; ++src, dest += audio_bus->channels())
+ *dest = *src;
+ }
+}
+
} // anonymous namespace
-// Nested class encapsulating opus-related encoding details.
-// AudioEncoder is created and destroyed on ATR's main thread (usually the
-// main render thread) but otherwise should operate entirely on
-// |encoder_thread_|, which is owned by AudioTrackRecorder. Be sure to delete
-// |encoder_thread_| before deleting the AudioEncoder using it.
+// Nested class encapsulating opus-related encoding details. It contains an
+// AudioConverter to adapt incoming data to the format Opus likes to have.
+// AudioEncoder is created and destroyed on ATR's main thread (usually the main
+// render thread) but otherwise should operate entirely on |encoder_thread_|,
+// which is owned by AudioTrackRecorder. Be sure to delete |encoder_thread_|
+// before deleting the AudioEncoder using it.
class AudioTrackRecorder::AudioEncoder
- : public base::RefCountedThreadSafe<AudioEncoder> {
+ : public base::RefCountedThreadSafe<AudioEncoder>,
+ public media::AudioConverter::InputCallback {
public:
AudioEncoder(const OnEncodedAudioCB& on_encoded_audio_cb,
int32_t bits_per_second);
@@ -56,17 +111,15 @@
private:
friend class base::RefCountedThreadSafe<AudioEncoder>;
- ~AudioEncoder();
+ ~AudioEncoder() override;
bool is_initialized() const { return !!opus_encoder_; }
- void DestroyExistingOpusEncoder();
+ // media::AudioConverted::InputCallback implementation.
+ double ProvideInput(media::AudioBus* audio_bus,
+ base::TimeDelta buffer_delay) override;
- void TransferSamplesIntoBuffer(const media::AudioBus* audio_bus,
- int source_offset,
- int buffer_fill_offset,
- int num_samples);
- bool EncodeFromFilledBuffer(std::string* out);
+ void DestroyExistingOpusEncoder();
const OnEncodedAudioCB on_encoded_audio_cb_;
@@ -75,17 +128,15 @@
base::ThreadChecker encoder_thread_checker_;
- // In the case where a call to EncodeAudio() cannot completely fill the
- // buffer, this points to the position at which to populate data in a later
- // call.
- int buffer_fill_end_;
+ // Track Audio (ingress) and Opus encoder input parameters, respectively. They
+ // only differ in their sample_rate() and frames_per_buffer(): output is
+ // 48ksamples/s and 2880, respectively.
+ media::AudioParameters input_params_;
+ media::AudioParameters output_params_;
- int frames_per_buffer_;
-
- // The duration of one set of frames of encoded audio samples.
- base::TimeDelta buffer_duration_;
-
- media::AudioParameters audio_params_;
+ // Sampling rate adapter between an OpusEncoder supported and the provided.
+ scoped_ptr<media::AudioConverter> converter_;
+ scoped_ptr<media::AudioFifo> fifo_;
// Buffer for passing AudioBus data to OpusEncoder.
scoped_ptr<float[]> buffer_;
@@ -115,50 +166,56 @@
}
void AudioTrackRecorder::AudioEncoder::OnSetFormat(
- const media::AudioParameters& params) {
+ const media::AudioParameters& input_params) {
+ DVLOG(1) << __FUNCTION__;
DCHECK(encoder_thread_checker_.CalledOnValidThread());
- if (audio_params_.Equals(params))
+ if (input_params_.Equals(input_params))
return;
DestroyExistingOpusEncoder();
- if (!params.IsValid() || params.channels() > 2) {
- DLOG(ERROR) << "Invalid audio params: " << params.AsHumanReadableString();
+ if (!input_params.IsValid()) {
+ DLOG(ERROR) << "Invalid params: " << input_params.AsHumanReadableString();
return;
}
+ input_params_ = input_params;
+ input_params_.set_frames_per_buffer(input_params_.sample_rate() *
+ kOpusPreferredBufferDurationMs /
+ base::Time::kMillisecondsPerSecond);
- buffer_duration_ = base::TimeDelta::FromMilliseconds(
- AudioTrackRecorder::GetOpusBufferDuration(params.sample_rate()));
- if (buffer_duration_ == base::TimeDelta()) {
- DLOG(ERROR) << "Could not find a valid |buffer_duration| for the given "
- << "sample rate: " << params.sample_rate();
- return;
- }
+ // third_party/libopus supports up to 2 channels (see implementation of
+ // opus_encoder_create()): force |output_params_| to at most those.
+ output_params_ = media::AudioParameters(
+ media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
+ media::GuessChannelLayout(std::min(input_params_.channels(), 2)),
+ kOpusPreferredSamplingRate,
+ input_params_.bits_per_sample(),
+ kOpusPreferredFramesPerBuffer);
+ DVLOG(1) << "|input_params_|:" << input_params_.AsHumanReadableString()
+ << " -->|output_params_|:" << output_params_.AsHumanReadableString();
- frames_per_buffer_ =
- params.sample_rate() * buffer_duration_.InMilliseconds() / 1000;
- if (frames_per_buffer_ * params.channels() > MAX_SAMPLES_PER_BUFFER) {
- DLOG(ERROR) << "Invalid |frames_per_buffer_|: " << frames_per_buffer_;
- return;
- }
+ converter_.reset(new media::AudioConverter(input_params_, output_params_,
+ false /* disable_fifo */));
+ converter_->AddInput(this);
+ converter_->PrimeWithSilence();
- // Initialize AudioBus buffer for OpusEncoder.
- buffer_fill_end_ = 0;
- buffer_.reset(new float[params.channels() * frames_per_buffer_]);
+ fifo_.reset(new media::AudioFifo(
+ input_params_.channels(),
+ kMaxNumberOfFifoBuffers * input_params_.frames_per_buffer()));
+
+ buffer_.reset(new float[output_params_.channels() *
+ output_params_.frames_per_buffer()]);
// Initialize OpusEncoder.
- DCHECK((params.sample_rate() != 48000) || (params.sample_rate() != 24000) ||
- (params.sample_rate() != 16000) || (params.sample_rate() != 12000) ||
- (params.sample_rate() != 8000))
- << "Opus supports only sample rates of {48, 24, 16, 12, 8}000, requested "
- << params.sample_rate();
int opus_result;
- opus_encoder_ = opus_encoder_create(params.sample_rate(), params.channels(),
- OPUS_APPLICATION_AUDIO, &opus_result);
+ opus_encoder_ = opus_encoder_create(output_params_.sample_rate(),
+ output_params_.channels(),
+ OPUS_APPLICATION_AUDIO,
+ &opus_result);
if (opus_result < 0) {
DLOG(ERROR) << "Couldn't init opus encoder: " << opus_strerror(opus_result)
- << ", sample rate: " << params.sample_rate()
- << ", channels: " << params.channels();
+ << ", sample rate: " << output_params_.sample_rate()
+ << ", channels: " << output_params_.channels();
return;
}
@@ -172,48 +229,52 @@
DLOG(ERROR) << "Failed to set opus bitrate: " << bitrate;
return;
}
-
- audio_params_ = params;
}
void AudioTrackRecorder::AudioEncoder::EncodeAudio(
- scoped_ptr<media::AudioBus> audio_bus,
+ scoped_ptr<media::AudioBus> input_bus,
const base::TimeTicks& capture_time) {
+ DVLOG(1) << __FUNCTION__ << ", #frames " << input_bus->frames();
DCHECK(encoder_thread_checker_.CalledOnValidThread());
- DCHECK_EQ(audio_bus->channels(), audio_params_.channels());
+ DCHECK_EQ(input_bus->channels(), input_params_.channels());
+ DCHECK(!capture_time.is_null());
+ DCHECK(converter_);
if (!is_initialized())
return;
+ // TODO(mcasas): Consider using a std::deque<scoped_ptr<AudioBus>> instead of
+ // an AudioFifo, to avoid copying data needlessly since we know the sizes of
+ // both input and output and they are multiples.
+ fifo_->Push(input_bus.get());
- base::TimeDelta buffer_fill_duration =
- buffer_fill_end_ * buffer_duration_ / frames_per_buffer_;
- base::TimeTicks buffer_capture_time = capture_time - buffer_fill_duration;
-
- // Encode all audio in |audio_bus| into zero or more packets.
- int src_pos = 0;
- while (src_pos < audio_bus->frames()) {
- const int num_samples_to_xfer = std::min(
- frames_per_buffer_ - buffer_fill_end_, audio_bus->frames() - src_pos);
- TransferSamplesIntoBuffer(audio_bus.get(), src_pos, buffer_fill_end_,
- num_samples_to_xfer);
- src_pos += num_samples_to_xfer;
- buffer_fill_end_ += num_samples_to_xfer;
-
- if (buffer_fill_end_ < frames_per_buffer_)
- break;
+ // Wait to have enough |input_bus|s to guarantee a satisfactory conversion.
+ while (fifo_->frames() >= input_params_.frames_per_buffer()) {
+ scoped_ptr<media::AudioBus> audio_bus = media::AudioBus::Create(
+ output_params_.channels(), kOpusPreferredFramesPerBuffer);
+ converter_->Convert(audio_bus.get());
+ ToInterleaved(audio_bus.get(), buffer_.get());
scoped_ptr<std::string> encoded_data(new std::string());
- if (EncodeFromFilledBuffer(encoded_data.get())) {
- on_encoded_audio_cb_.Run(audio_params_, std::move(encoded_data),
- buffer_capture_time);
+ if (DoEncode(opus_encoder_, buffer_.get(), kOpusPreferredFramesPerBuffer,
+ encoded_data.get())) {
+ const base::TimeTicks capture_time_of_first_sample =
+ capture_time -
+ base::TimeDelta::FromMicroseconds(fifo_->frames() *
+ base::Time::kMicrosecondsPerSecond /
+ input_params_.sample_rate());
+ on_encoded_audio_cb_.Run(output_params_, std::move(encoded_data),
+ capture_time_of_first_sample);
}
-
- // Reset the capture timestamp and internal buffer for next set of frames.
- buffer_capture_time += buffer_duration_;
- buffer_fill_end_ = 0;
}
}
+double AudioTrackRecorder::AudioEncoder::ProvideInput(
+ media::AudioBus* audio_bus,
+ base::TimeDelta buffer_delay) {
+ fifo_->Consume(audio_bus, 0, audio_bus->frames());
+ return 1.0; // Return volume greater than zero to indicate we have more data.
+}
+
void AudioTrackRecorder::AudioEncoder::DestroyExistingOpusEncoder() {
// We don't DCHECK that we're on the encoder thread here, as this could be
// called from the dtor (main thread) or from OnSetForamt() (render thread);
@@ -223,48 +284,6 @@
}
}
-void AudioTrackRecorder::AudioEncoder::TransferSamplesIntoBuffer(
- const media::AudioBus* audio_bus,
- int source_offset,
- int buffer_fill_offset,
- int num_samples) {
- // TODO(ajose): Consider replacing with AudioBus::ToInterleaved().
- // http://crbug.com/547918
- DCHECK(encoder_thread_checker_.CalledOnValidThread());
- DCHECK(is_initialized());
- // Opus requires channel-interleaved samples in a single array.
- for (int ch = 0; ch < audio_bus->channels(); ++ch) {
- const float* src = audio_bus->channel(ch) + source_offset;
- const float* const src_end = src + num_samples;
- float* dest =
- buffer_.get() + buffer_fill_offset * audio_params_.channels() + ch;
- for (; src < src_end; ++src, dest += audio_params_.channels())
- *dest = *src;
- }
-}
-
-bool AudioTrackRecorder::AudioEncoder::EncodeFromFilledBuffer(
- std::string* out) {
- DCHECK(encoder_thread_checker_.CalledOnValidThread());
- DCHECK(is_initialized());
-
- out->resize(OPUS_MAX_PAYLOAD_SIZE);
- const opus_int32 result = opus_encode_float(
- opus_encoder_, buffer_.get(), frames_per_buffer_,
- reinterpret_cast<uint8_t*>(string_as_array(out)), OPUS_MAX_PAYLOAD_SIZE);
- if (result > 1) {
- // TODO(ajose): Investigate improving this. http://crbug.com/547918
- out->resize(result);
- return true;
- }
- // If |result| in {0,1}, do nothing; the documentation says that a return
- // value of zero or one means the packet does not need to be transmitted.
- // Otherwise, we have an error.
- DLOG_IF(ERROR, result < 0) << __FUNCTION__
- << " failed: " << opus_strerror(result);
- return false;
-}
-
AudioTrackRecorder::AudioTrackRecorder(
const blink::WebMediaStreamTrack& track,
const OnEncodedAudioCB& on_encoded_audio_cb,
@@ -316,22 +335,4 @@
base::Passed(&audio_data), capture_time));
}
-int AudioTrackRecorder::GetOpusBufferDuration(int sample_rate) {
- // Valid buffer durations in millseconds. Note there are other valid
- // durations for Opus, see https://tools.ietf.org/html/rfc6716#section-2.1.4
- // Descending order as longer durations can increase compression performance.
- const std::vector<int> opus_valid_buffer_durations_ms = {60, 40, 20, 10};
-
- // Search for a duration such that |sample_rate| % |buffers_per_second| == 0,
- // where |buffers_per_second| = 1000ms / |possible_duration|.
- for (auto possible_duration : opus_valid_buffer_durations_ms) {
- if (sample_rate * possible_duration % 1000 == 0) {
- return possible_duration;
- }
- }
-
- // Otherwise, couldn't find a good duration.
- return 0;
-}
-
} // namespace content
diff --git a/content/renderer/media/audio_track_recorder.h b/content/renderer/media/audio_track_recorder.h
index 1d1a395c..7f7c1f9 100644
--- a/content/renderer/media/audio_track_recorder.h
+++ b/content/renderer/media/audio_track_recorder.h
@@ -17,6 +17,7 @@
namespace media {
class AudioBus;
+class AudioParameters;
} // namespace media
namespace content {
@@ -47,17 +48,10 @@
base::TimeTicks capture_time) override;
private:
- friend class AudioTrackRecorderTest;
- class AudioParameters;
-
// Forward declaration of nested class for handling encoding.
// See the implementation file for details.
class AudioEncoder;
- // Returns the Opus buffer duration in milliseconds, or zero if none will work
- // for the given |sample_rate|.
- static int GetOpusBufferDuration(int sample_rate);
-
// Used to check that we are destroyed on the same thread we were created on.
base::ThreadChecker main_render_thread_checker_;
diff --git a/content/renderer/media/audio_track_recorder_unittest.cc b/content/renderer/media/audio_track_recorder_unittest.cc
index 07c0b53..2c5d23f 100644
--- a/content/renderer/media/audio_track_recorder_unittest.cc
+++ b/content/renderer/media/audio_track_recorder_unittest.cc
@@ -31,14 +31,19 @@
namespace {
-// Input audio format.
-const media::AudioParameters::Format kDefaultInputFormat =
- media::AudioParameters::AUDIO_PCM_LOW_LATENCY;
const int kDefaultBitsPerSample = 16;
const int kDefaultSampleRate = 48000;
// The |frames_per_buffer| field of AudioParameters is not used by ATR.
const int kIgnoreFramesPerBuffer = 1;
-const int kOpusMaxBufferDurationMS = 120;
+
+// The following parameters replicate those in audio_track_recorder.cc, see this
+// file for explanations.
+const int kMediaStreamAudioTrackBufferDurationMs = 10;
+const int kOpusBufferDurationMs = 60;
+const int kRatioInputToOutputFrames =
+ kOpusBufferDurationMs / kMediaStreamAudioTrackBufferDurationMs;
+
+const int kFramesPerBuffer = kOpusBufferDurationMs * kDefaultSampleRate / 1000;
} // namespace
@@ -62,13 +67,32 @@
kDefaultSampleRate, /* sample rate */
kDefaultBitsPerSample}, /* bits per sample */
// Change to mono:
- {media::AudioParameters::AUDIO_PCM_LOW_LATENCY, media::CHANNEL_LAYOUT_MONO,
- kDefaultSampleRate, kDefaultBitsPerSample},
- // Different sampling rate as well:
- {media::AudioParameters::AUDIO_PCM_LOW_LATENCY, media::CHANNEL_LAYOUT_MONO,
- 24000, kDefaultBitsPerSample},
{media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
- media::CHANNEL_LAYOUT_STEREO, 8000, kDefaultBitsPerSample},
+ media::CHANNEL_LAYOUT_MONO,
+ kDefaultSampleRate,
+ kDefaultBitsPerSample},
+ // Different sampling rate as well:
+ {media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
+ media::CHANNEL_LAYOUT_MONO,
+ 24000,
+ kDefaultBitsPerSample},
+ {media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
+ media::CHANNEL_LAYOUT_STEREO,
+ 8000,
+ kDefaultBitsPerSample},
+ // Using a non-default Opus sampling rate (48, 24, 16, 12, or 8 kHz).
+ {media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
+ media::CHANNEL_LAYOUT_MONO,
+ 22050,
+ kDefaultBitsPerSample},
+ {media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
+ media::CHANNEL_LAYOUT_STEREO,
+ 44100,
+ kDefaultBitsPerSample},
+ {media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
+ media::CHANNEL_LAYOUT_STEREO,
+ 96000,
+ kDefaultBitsPerSample},
};
class AudioTrackRecorderTest : public TestWithParam<ATRTestParams> {
@@ -81,7 +105,7 @@
GetParam().sample_rate,
GetParam().bits_per_sample,
kIgnoreFramesPerBuffer),
- second_params_(kDefaultInputFormat,
+ second_params_(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
media::CHANNEL_LAYOUT_STEREO,
kDefaultSampleRate,
kDefaultBitsPerSample,
@@ -104,9 +128,11 @@
~AudioTrackRecorderTest() {
opus_decoder_destroy(opus_decoder_);
opus_decoder_ = nullptr;
- audio_track_recorder_.reset();
blink_track_.reset();
blink::WebHeap::collectAllGarbageForTesting();
+ audio_track_recorder_.reset();
+ // Let the message loop run to finish destroying the recorder properly.
+ base::RunLoop().RunUntilIdle();
}
void ResetDecoder(const media::AudioParameters& params) {
@@ -117,31 +143,25 @@
int error;
opus_decoder_ =
- opus_decoder_create(params.sample_rate(), params.channels(), &error);
+ opus_decoder_create(kDefaultSampleRate, params.channels(), &error);
EXPECT_TRUE(error == OPUS_OK && opus_decoder_);
- max_frames_per_buffer_ =
- kOpusMaxBufferDurationMS * params.sample_rate() / 1000;
- buffer_.reset(new float[max_frames_per_buffer_ * params.channels()]);
+ buffer_.reset(new float[kFramesPerBuffer * params.channels()]);
}
scoped_ptr<media::AudioBus> GetFirstSourceAudioBus() {
scoped_ptr<media::AudioBus> bus(media::AudioBus::Create(
- first_params_.channels(),
- first_params_.sample_rate() *
- audio_track_recorder_->GetOpusBufferDuration(
- first_params_.sample_rate()) /
- 1000));
+ first_params_.channels(), first_params_.sample_rate() *
+ kMediaStreamAudioTrackBufferDurationMs /
+ base::Time::kMillisecondsPerSecond));
first_source_.OnMoreData(bus.get(), 0, 0);
return bus;
}
scoped_ptr<media::AudioBus> GetSecondSourceAudioBus() {
scoped_ptr<media::AudioBus> bus(media::AudioBus::Create(
- second_params_.channels(),
- second_params_.sample_rate() *
- audio_track_recorder_->GetOpusBufferDuration(
- second_params_.sample_rate()) /
- 1000));
+ second_params_.channels(), second_params_.sample_rate() *
+ kMediaStreamAudioTrackBufferDurationMs /
+ base::Time::kMillisecondsPerSecond));
second_source_.OnMoreData(bus.get(), 0, 0);
return bus;
}
@@ -155,17 +175,13 @@
scoped_ptr<std::string> encoded_data,
base::TimeTicks timestamp) {
EXPECT_TRUE(!encoded_data->empty());
-
// Decode |encoded_data| and check we get the expected number of frames
// per buffer.
- EXPECT_EQ(
- params.sample_rate() *
- audio_track_recorder_->GetOpusBufferDuration(params.sample_rate()) /
- 1000,
- opus_decode_float(
- opus_decoder_,
- reinterpret_cast<uint8_t*>(string_as_array(encoded_data.get())),
- encoded_data->size(), buffer_.get(), max_frames_per_buffer_, 0));
+ EXPECT_EQ(kDefaultSampleRate * kOpusBufferDurationMs / 1000,
+ opus_decode_float(
+ opus_decoder_, reinterpret_cast<uint8_t*>(
+ string_as_array(encoded_data.get())),
+ encoded_data->size(), buffer_.get(), kFramesPerBuffer, 0));
DoOnEncodedAudio(params, *encoded_data, timestamp);
}
@@ -186,7 +202,6 @@
// Decoder for verifying data was properly encoded.
OpusDecoder* opus_decoder_;
- int max_frames_per_buffer_;
scoped_ptr<float[]> buffer_;
private:
@@ -223,30 +238,49 @@
// Give ATR initial audio parameters.
audio_track_recorder_->OnSetFormat(first_params_);
+
// TODO(ajose): consider adding WillOnce(SaveArg...) and inspecting, as done
// in VTR unittests. http://crbug.com/548856
- const base::TimeTicks time1 = base::TimeTicks::Now();
- EXPECT_CALL(*this, DoOnEncodedAudio(_, _, time1)).Times(1);
- audio_track_recorder_->OnData(*GetFirstSourceAudioBus(), time1);
+ EXPECT_CALL(*this, DoOnEncodedAudio(_, _, _)).Times(1);
+ audio_track_recorder_->OnData(*GetFirstSourceAudioBus(),
+ base::TimeTicks::Now());
+ for (int i = 0; i < kRatioInputToOutputFrames - 1; ++i) {
+ audio_track_recorder_->OnData(*GetFirstSourceAudioBus(),
+ base::TimeTicks::Now());
+ }
- // Send more audio.
- const base::TimeTicks time2 = base::TimeTicks::Now();
EXPECT_CALL(*this, DoOnEncodedAudio(_, _, _))
.Times(1)
// Only reset the decoder once we've heard back:
.WillOnce(RunClosure(base::Bind(&AudioTrackRecorderTest::ResetDecoder,
base::Unretained(this), second_params_)));
- audio_track_recorder_->OnData(*GetFirstSourceAudioBus(), time2);
+ audio_track_recorder_->OnData(*GetFirstSourceAudioBus(),
+ base::TimeTicks::Now());
+ for (int i = 0; i < kRatioInputToOutputFrames - 1; ++i) {
+ audio_track_recorder_->OnData(*GetFirstSourceAudioBus(),
+ base::TimeTicks::Now());
+ }
+
+ // If the amount of samples/10ms buffer is not an integer (e.g. 22050Hz) we
+ // need an extra OnData() to account for the round-off error.
+ if (GetParam().sample_rate % 100) {
+ audio_track_recorder_->OnData(*GetFirstSourceAudioBus(),
+ base::TimeTicks::Now());
+ }
// Give ATR new audio parameters.
audio_track_recorder_->OnSetFormat(second_params_);
// Send audio with different params.
- const base::TimeTicks time3 = base::TimeTicks::Now();
EXPECT_CALL(*this, DoOnEncodedAudio(_, _, _))
.Times(1)
.WillOnce(RunClosure(quit_closure));
- audio_track_recorder_->OnData(*GetSecondSourceAudioBus(), time3);
+ audio_track_recorder_->OnData(*GetSecondSourceAudioBus(),
+ base::TimeTicks::Now());
+ for (int i = 0; i < kRatioInputToOutputFrames - 1; ++i) {
+ audio_track_recorder_->OnData(*GetSecondSourceAudioBus(),
+ base::TimeTicks::Now());
+ }
run_loop.Run();
Mock::VerifyAndClearExpectations(this);
diff --git a/content/renderer/media/media_recorder_handler_unittest.cc b/content/renderer/media/media_recorder_handler_unittest.cc
index 4a92b22..2a99ccb 100644
--- a/content/renderer/media/media_recorder_handler_unittest.cc
+++ b/content/renderer/media/media_recorder_handler_unittest.cc
@@ -22,6 +22,7 @@
using ::testing::_;
using ::testing::AtLeast;
using ::testing::InSequence;
+using ::testing::Gt;
using ::testing::Lt;
using ::testing::Mock;
using ::testing::TestWithParam;
@@ -41,25 +42,24 @@
static const int kTestAudioChannels = 2;
static const int kTestAudioBitsPerSample = 16;
static const int kTestAudioSampleRate = 48000;
-static const int kTestAudioBufferDurationMS = 60;
+static const int kTestAudioBufferDurationMs = 10;
+// Opus works with 60ms buffers, so 6 MediaStreamAudioTrack Buffers are needed
+// to encode one output buffer.
+static const int kRatioOpusToTestAudioBuffers = 6;
struct MediaRecorderTestParams {
const bool has_video;
const bool has_audio;
const char* const mime_type;
const char* const codecs;
- const size_t first_encoded_video_frame_size;
- const size_t second_encoded_video_frame_size;
- const size_t first_encoded_audio_frame_size;
- const size_t second_encoded_audio_frame_size;
};
// Array of valid combinations of video/audio/codecs and expected collected
// encoded sizes to use for parameterizing MediaRecorderHandlerTest.
static const MediaRecorderTestParams kMediaRecorderTestParams[] = {
- {true, false, "video/webm", "vp8", 52, 32, 0, 0},
- {true, false, "video/webm", "vp9", 33, 18, 0, 0},
- {false, true, "video/webm", "vp8", 0, 0, 990, 706}};
+ {true, false, "video/webm", "vp8"},
+ {true, false, "video/webm", "vp9"},
+ {false, true, "video/webm", "vp8"}};
class MediaRecorderHandlerTest : public TestWithParam<MediaRecorderTestParams>,
public blink::WebMediaRecorderHandlerClient {
@@ -113,7 +113,7 @@
scoped_ptr<media::AudioBus> NextAudioBus() {
scoped_ptr<media::AudioBus> bus(media::AudioBus::Create(
kTestAudioChannels,
- kTestAudioSampleRate * kTestAudioBufferDurationMS / 1000));
+ kTestAudioSampleRate * kTestAudioBufferDurationMs / 1000));
audio_source_.OnMoreData(bus.get(), 0, 0);
return bus;
}
@@ -221,31 +221,31 @@
const scoped_refptr<media::VideoFrame> video_frame =
media::VideoFrame::CreateBlackFrame(gfx::Size(160, 80));
+ const size_t kEncodedSizeThreshold = 16;
{
base::RunLoop run_loop;
base::Closure quit_closure = run_loop.QuitClosure();
// writeData() is pinged a number of times as the WebM header is written;
// the last time it is called it has the encoded data.
- const size_t encoded_data_size = GetParam().first_encoded_video_frame_size;
- EXPECT_CALL(*this, writeData(_, Lt(encoded_data_size), _))
+ EXPECT_CALL(*this, writeData(_, Lt(kEncodedSizeThreshold), _))
.Times(AtLeast(1));
- EXPECT_CALL(*this, writeData(_, encoded_data_size, _))
+ EXPECT_CALL(*this, writeData(_, Gt(kEncodedSizeThreshold), _))
.Times(1)
.WillOnce(RunClosure(quit_closure));
OnVideoFrameForTesting(video_frame);
run_loop.Run();
}
+ Mock::VerifyAndClearExpectations(this);
{
base::RunLoop run_loop;
base::Closure quit_closure = run_loop.QuitClosure();
// The second time around writeData() is called a number of times to write
// the WebM frame header, and then is pinged with the encoded data.
- const size_t encoded_data_size = GetParam().second_encoded_video_frame_size;
- EXPECT_CALL(*this, writeData(_, Lt(encoded_data_size), _))
+ EXPECT_CALL(*this, writeData(_, Lt(kEncodedSizeThreshold), _))
.Times(AtLeast(1));
- EXPECT_CALL(*this, writeData(_, encoded_data_size, _))
+ EXPECT_CALL(*this, writeData(_, Gt(kEncodedSizeThreshold), _))
.Times(1)
.WillOnce(RunClosure(quit_closure));
@@ -284,38 +284,40 @@
media::AudioParameters params(
media::AudioParameters::AUDIO_PCM_LINEAR, media::CHANNEL_LAYOUT_STEREO,
kTestAudioSampleRate, kTestAudioBitsPerSample,
- kTestAudioSampleRate * kTestAudioBufferDurationMS / 1000);
+ kTestAudioSampleRate * kTestAudioBufferDurationMs / 1000);
SetAudioFormatForTesting(params);
+ const size_t kEncodedSizeThreshold = 24;
{
base::RunLoop run_loop;
base::Closure quit_closure = run_loop.QuitClosure();
// writeData() is pinged a number of times as the WebM header is written;
// the last time it is called it has the encoded data.
- const size_t kEncodedDataSize = GetParam().first_encoded_audio_frame_size;
- EXPECT_CALL(*this, writeData(_, Lt(kEncodedDataSize), _)).Times(AtLeast(1));
- EXPECT_CALL(*this, writeData(_, kEncodedDataSize, _))
+ EXPECT_CALL(*this, writeData(_, Lt(kEncodedSizeThreshold), _))
+ .Times(AtLeast(1));
+ EXPECT_CALL(*this, writeData(_, Gt(kEncodedSizeThreshold), _))
.Times(1)
.WillOnce(RunClosure(quit_closure));
- OnAudioBusForTesting(*audio_bus1);
+ for (int i = 0; i < kRatioOpusToTestAudioBuffers; ++i)
+ OnAudioBusForTesting(*audio_bus1);
run_loop.Run();
}
+ Mock::VerifyAndClearExpectations(this);
{
base::RunLoop run_loop;
base::Closure quit_closure = run_loop.QuitClosure();
// The second time around writeData() is called a number of times to write
// the WebM frame header, and then is pinged with the encoded data.
- const size_t kSecondEncodedDataSize =
- GetParam().second_encoded_audio_frame_size;
- EXPECT_CALL(*this, writeData(_, Lt(kSecondEncodedDataSize), _))
+ EXPECT_CALL(*this, writeData(_, Lt(kEncodedSizeThreshold), _))
.Times(AtLeast(1));
- EXPECT_CALL(*this, writeData(_, kSecondEncodedDataSize, _))
+ EXPECT_CALL(*this, writeData(_, Gt(kEncodedSizeThreshold), _))
.Times(1)
.WillOnce(RunClosure(quit_closure));
- OnAudioBusForTesting(*audio_bus2);
+ for (int i = 0; i < kRatioOpusToTestAudioBuffers; ++i)
+ OnAudioBusForTesting(*audio_bus2);
run_loop.Run();
}