Chromium Code Reviews| 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 |