Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(647)

Unified Diff: content/renderer/media/audio_track_recorder.cc

Issue 1406113002: Add AudioTrackRecorder for audio component of MediaStream recording. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: trybots Created 5 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: content/renderer/media/audio_track_recorder.cc
diff --git a/content/renderer/media/audio_track_recorder.cc b/content/renderer/media/audio_track_recorder.cc
new file mode 100644
index 0000000000000000000000000000000000000000..9a44bfb18b7854be2685c5fd6995854d07f1b246
--- /dev/null
+++ b/content/renderer/media/audio_track_recorder.cc
@@ -0,0 +1,266 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/bind.h"
+#include "base/stl_util.h"
+#include "base/threading/thread.h"
+#include "content/renderer/media/audio_track_recorder.h"
+
+#include "third_party/opus/src/include/opus.h"
+
+namespace content {
+
+namespace {
+
+// TODO(ajose): This likely shouldn't be hardcoded.
+const int kDefaultFramesPerSecond = 100;
miu 2015/10/21 20:35:38 Consider using enums for these constants rather th
ajose 2015/10/23 22:46:33 Done.
+// This is the recommended value, 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.
+//
+// Note: Whereas other RTP implementations do not, the cast library is
miu 2015/10/21 20:35:38 This comment seems....out of place? ;-)
ajose 2015/10/23 22:46:33 Whoops...
+// perfectly capable of transporting larger than MTU-sized audio frames.
+static const int kOpusMaxPayloadSize = 4000;
+
+// TODO(ajose): Removed code that might be useful for ensuring A/V sync.
+// See cast/sender/AudioEncoder.
+
+} // anonymous namespace
+
+class AudioTrackRecorder::AudioEncoder
miu 2015/10/21 20:35:38 General statement about the entire AudioEncoder cl
ajose 2015/10/23 22:46:33 Done.
+ : public base::RefCountedThreadSafe<AudioEncoder> {
+ public:
+ AudioEncoder(const OnEncodedAudioCB& on_encoded_audio_cb)
miu 2015/10/21 20:35:38 Need explicit keyword here.
ajose 2015/10/23 22:46:33 Done.
+ : on_encoded_audio_cb_(on_encoded_audio_cb),
mcasas 2015/10/21 19:19:51 Don't inline complex ctor.
miu 2015/10/21 20:35:38 It doesn't matter for classes private to .cc modul
ajose 2015/10/23 22:46:33 Acknowledged.
ajose 2015/10/23 22:46:33 Done.
+ main_task_runner_(base::MessageLoop::current()->task_runner()) {}
+
+ void OnSetFormat(const media::AudioParameters& params) { InitOpus(params); }
+
+ void EncodeAudio(const media::AudioBus& audio_bus,
+ const base::TimeTicks& recorded_time);
+
+ bool IsInitialized() { return audio_params_.get() != nullptr; }
+
+ private:
+ friend class base::RefCountedThreadSafe<AudioEncoder>;
+
+ ~AudioEncoder();
+
+ void InitOpus(const media::AudioParameters& params);
+
+ void TransferSamplesIntoBuffer(const media::AudioBus& audio_bus,
+ int source_offset,
+ int buffer_fill_offset,
+ int num_samples);
+ bool EncodeFromFilledBuffer(std::string* out);
+
+ static bool IsValidFrameDuration(base::TimeDelta duration);
+
+ int samples_per_frame_;
+ const OnEncodedAudioCB on_encoded_audio_cb_;
+
+ // 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_;
+
+ // Used to shutdown properly on the same thread we were created on.
+ const scoped_refptr<base::SingleThreadTaskRunner> main_task_runner_;
mcasas 2015/10/21 19:19:52 I think this fellow is not used now, perhaps you w
ajose 2015/10/23 22:46:33 ThreadChecker already does something similar to th
+
+ // Task runner where frames to encode and reply callbacks must happen.
+ scoped_refptr<base::SingleThreadTaskRunner> origin_task_runner_;
+
+ scoped_ptr<media::AudioParameters> audio_params_;
miu 2015/10/21 20:35:38 No need for separate heap allocation here. Just:
ajose 2015/10/23 22:46:33 Done.
+
+ // Buffer for passing AudioBus data to OpusEncoder.
+ scoped_ptr<float[]> buffer_;
+
+ OpusEncoder* opus_encoder_;
mcasas 2015/10/21 19:19:52 Hmm this smells like a base::WeakPtr<>. However, t
ajose 2015/10/23 22:46:33 Think this is resolved with new structure - AudioE
+
+ DISALLOW_COPY_AND_ASSIGN(AudioEncoder);
+};
+
+AudioTrackRecorder::AudioEncoder::~AudioEncoder() {
+ LOG(INFO) << "AudioEncoder dtor";
+ if (opus_encoder_) {
+ LOG(INFO) << "destroying opus";
+ opus_encoder_destroy(opus_encoder_);
+ opus_encoder_ = nullptr;
+ }
+}
+
+void AudioTrackRecorder::AudioEncoder::EncodeAudio(
+ const media::AudioBus& audio_bus,
+ const base::TimeTicks& recorded_time) {
+ DCHECK(IsInitialized());
+ DCHECK(!recorded_time.is_null());
+ DCHECK_EQ(audio_bus.channels(), audio_params_->channels());
+ DCHECK_EQ(audio_bus.frames(), audio_params_->frames_per_buffer());
+
+ if (!origin_task_runner_.get())
+ origin_task_runner_ = base::MessageLoop::current()->task_runner();
miu 2015/10/21 20:35:38 This doesn't seem safe to me. Usually encoding sh
ajose 2015/10/23 22:46:33 Done.
+ DCHECK(origin_task_runner_->BelongsToCurrentThread());
+
+ // Encode all audio in |audio_bus| into zero or more frames.
+ int src_pos = 0;
+ while (src_pos < audio_bus.frames()) {
+ const int num_samples_to_xfer = std::min(
+ samples_per_frame_ - buffer_fill_end_, audio_bus.frames() - src_pos);
+ TransferSamplesIntoBuffer(audio_bus, 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_ < samples_per_frame_)
+ break;
+
+ scoped_ptr<std::string> encoded_data(new std::string());
+ if (EncodeFromFilledBuffer(encoded_data.get())) {
+ origin_task_runner_->PostTask(
+ FROM_HERE, base::Bind(on_encoded_audio_cb_, *audio_params_,
+ base::Passed(&encoded_data), recorded_time));
+ }
+
+ // Reset the internal buffer for the next frame.
+ buffer_fill_end_ = 0;
+ }
+}
+
+void AudioTrackRecorder::AudioEncoder::InitOpus(
+ const media::AudioParameters& params) {
mcasas 2015/10/21 19:19:52 Thread check plz, here and elsewhere.
ajose 2015/10/23 22:46:33 Done.
+ DCHECK(params.IsValid());
+ DCHECK_EQ(params.bits_per_sample(), 16);
+ if (audio_params_.get() && audio_params_->Equals(params))
mcasas 2015/10/21 19:19:52 How can |audio_params_.get()| be nullptr if it's a
+ return;
+
+ const int sampling_rate = params.sample_rate();
+ const int num_channels = params.channels();
+ samples_per_frame_ = sampling_rate / kDefaultFramesPerSecond;
+ const base::TimeDelta frame_duration = base::TimeDelta::FromMicroseconds(
+ base::Time::kMicrosecondsPerSecond * samples_per_frame_ / sampling_rate);
+
+ if (frame_duration == base::TimeDelta() ||
+ !IsValidFrameDuration(frame_duration)) {
+ DVLOG(1) << __FUNCTION__ << ": bad frame duration: " << frame_duration;
+ }
+
+ // Support for max sampling rate of 48KHz, 2 channels, 100 ms duration.
+ const int kMaxSamplesTimesChannelsPerFrame = 48 * 2 * 100;
+ if (num_channels <= 0 || samples_per_frame_ <= 0 ||
+ samples_per_frame_ * num_channels > kMaxSamplesTimesChannelsPerFrame ||
+ sampling_rate % samples_per_frame_ != 0) {
mcasas 2015/10/21 19:19:52 I see what you do here but is more customary to c
ajose 2015/10/23 22:46:33 Might have gone overboard...
+ DVLOG(1) << __FUNCTION__ << ": bad inputs:"
+ << "\nnum channels: " << num_channels
+ << "\nsamples per frame: " << samples_per_frame_
+ << "\nsampling rate: " << sampling_rate;
+ return;
+ }
+
+ // Initialize AudioBus buffer for OpusEncoder.
+ buffer_fill_end_ = 0;
+ buffer_.reset(new float[num_channels * samples_per_frame_]);
+
+ // Initialize OpusEncoder.
+ int opus_result;
+ opus_encoder_ = opus_encoder_create(sampling_rate, num_channels,
miu 2015/10/21 20:35:38 Can this InitOpus() method be called multiple time
ajose 2015/10/23 22:46:33 Done.
+ OPUS_APPLICATION_AUDIO, &opus_result);
+ if (opus_result < 0) {
+ DVLOG(1) << __FUNCTION__ << ": couldn't initialize opus encoder: "
mcasas 2015/10/21 19:19:52 There's no real need for __FUNCTION__ here, and I'
ajose 2015/10/23 22:46:33 Done.
+ << opus_strerror(opus_result);
+ return;
+ }
+
+ audio_params_.reset(new media::AudioParameters(params));
+
+ // Note: As of 2013-10-31, the encoder in "auto bitrate" mode would use a
+ // variable bitrate up to 102kbps for 2-channel, 48 kHz audio and a 10 ms
+ // frame size. The opus library authors may, of course, adjust this in
+ // later versions.
mcasas 2015/10/21 19:19:52 Could we codify this assumption? int bitrate;
+ CHECK_EQ(opus_encoder_ctl(opus_encoder_, OPUS_SET_BITRATE(OPUS_AUTO)),
mcasas 2015/10/21 19:19:51 Here you say: crash the renderer tab if opus_encod
ajose 2015/10/23 22:46:33 Done.
+ OPUS_OK);
+}
+
+void AudioTrackRecorder::AudioEncoder::TransferSamplesIntoBuffer(
+ // const media::AudioBus* audio_bus,
+ const media::AudioBus& audio_bus,
+ int source_offset,
+ int buffer_fill_offset,
+ int num_samples) {
+ // 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) {
+ out->resize(kOpusMaxPayloadSize);
+ const opus_int32 result = opus_encode_float(
+ opus_encoder_, buffer_.get(), samples_per_frame_,
+ reinterpret_cast<uint8*>(string_as_array(out)), kOpusMaxPayloadSize);
mcasas 2015/10/21 19:19:52 What about s/reinterpret_cast<uint8*>(string_as_a
ajose 2015/10/23 22:46:33 out->data() is const char*, need unsigned char* bu
+ if (result > 1) {
+ out->resize(result);
+ return true;
+ } else if (result < 0) {
+ LOG(ERROR) << __FUNCTION__ << ": Error code from opus_encode_float(): "
+ << opus_strerror(result);
+ return false;
+ } else {
+ // Do nothing: The documentation says that a return value of zero or
+ // one means the packet does not need to be transmitted.
+ return false;
+ }
mcasas 2015/10/21 19:19:52 What about: if (result > 1) { out->resize(resul
ajose 2015/10/23 22:46:33 Done.
+}
+
+// static
+bool AudioTrackRecorder::AudioEncoder::IsValidFrameDuration(
+ base::TimeDelta duration) {
+ // See https://tools.ietf.org/html/rfc6716#section-2.1.4
mcasas 2015/10/21 19:19:52 Maybe also mention opus.h? [1] [1] https://code.g
+ return duration == base::TimeDelta::FromMicroseconds(2500) ||
+ duration == base::TimeDelta::FromMilliseconds(5) ||
+ duration == base::TimeDelta::FromMilliseconds(10) ||
+ duration == base::TimeDelta::FromMilliseconds(20) ||
+ duration == base::TimeDelta::FromMilliseconds(40) ||
+ duration == base::TimeDelta::FromMilliseconds(60);
mcasas 2015/10/21 19:19:52 It sucks that we have to check for this duration o
ajose 2015/10/23 22:46:33 Not that I can find :(
+}
+
+AudioTrackRecorder::AudioTrackRecorder(
+ const blink::WebMediaStreamTrack& track,
+ const OnEncodedAudioCB& on_encoded_audio_cb)
+ : track_(track),
+ encoder_(new AudioEncoder(on_encoded_audio_cb)),
miu 2015/10/21 20:35:38 Note: If AudioEncoder was only used on a separate
ajose 2015/10/23 22:46:33 Nice!
+ on_encoded_audio_cb_(on_encoded_audio_cb) {
+ DCHECK(main_render_thread_checker_.CalledOnValidThread());
+ DCHECK(!track_.isNull());
+ DCHECK(track_.extraData());
+ // Connect the source provider to the track as a sink.
+ MediaStreamAudioSink::AddToAudioTrack(this, track_);
+}
+
+AudioTrackRecorder::~AudioTrackRecorder() {
+ DCHECK(main_render_thread_checker_.CalledOnValidThread());
+ MediaStreamAudioSink::RemoveFromAudioTrack(this, track_);
+ track_.reset();
+ LOG(INFO) << "ATR dtor";
+}
+
+void AudioTrackRecorder::OnData(const media::AudioBus& audio_bus,
+ base::TimeTicks estimated_capture_time) {
+ DCHECK(capture_thread_checker_.CalledOnValidThread());
+ encoder_->EncodeAudio(audio_bus, estimated_capture_time);
miu 2015/10/21 20:35:38 Is this method being called on a real-time audio t
ajose 2015/10/23 22:46:33 Done.
+}
+
+void AudioTrackRecorder::OnSetFormat(const media::AudioParameters& params) {
+ // If the source is restarted, might have changed to another capture thread.
+ capture_thread_checker_.DetachFromThread();
+ DCHECK(capture_thread_checker_.CalledOnValidThread());
+ encoder_->OnSetFormat(params);
+}
+
+} // namespace content

Powered by Google App Engine
This is Rietveld 408576698