Skip to content

Commit

Permalink
MediaCaptureFromElement: add support for audio captureStream().
Browse files Browse the repository at this point in the history
This CL extends support for capturing the audio part of
a <video> or <audio> tags ( "capture" here means creating
a MediaStream out of the HTMLElement)

It introduces an HtmlAudioCapturerSource is-a AudioCapturerSource
wrapped into an ExternalMediaStreamAudioSource to produce data
towards the audio track.

HtmlAudioCapturerSource also plugs into the
WebMediaPlayer's WebAudioSourceProviderImpl to get
a copy of the audio being rendered.

Unit tests are added, and the existing LayouTests
revamped (and split into several files for clarity).

BUG=569976, 575492

TEST= run chromium with
 --enable-blink-features=MediaCaptureFromVideo
 against e.g.
https://rawgit.com/Miguelao/demos/master/videoelementcapture.html

Review-Url: https://codereview.chromium.org/1599533003
Cr-Commit-Position: refs/heads/master@{#395205}
  • Loading branch information
yellowdoge authored and Commit bot committed May 20, 2016
1 parent c612179 commit 77d0d44
Show file tree
Hide file tree
Showing 15 changed files with 445 additions and 54 deletions.
2 changes: 2 additions & 0 deletions content/content_renderer.gypi
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@
'renderer/media/cdm/ppapi_decryptor.h',
'renderer/media/cdm/render_cdm_factory.cc',
'renderer/media/cdm/render_cdm_factory.h',
'renderer/media/html_audio_element_capturer_source.cc',
'renderer/media/html_audio_element_capturer_source.h',
'renderer/media/external_media_stream_audio_source.cc',
'renderer/media/external_media_stream_audio_source.h',
'renderer/media/media_permission_dispatcher.cc',
Expand Down
1 change: 1 addition & 0 deletions content/content_tests.gypi
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,7 @@
'renderer/media/android/media_info_loader_unittest.cc',
'renderer/media/audio_message_filter_unittest.cc',
'renderer/media/audio_renderer_mixer_manager_unittest.cc',
'renderer/media/html_audio_element_capturer_source_unittest.cc',
'renderer/media/media_stream_audio_unittest.cc',
'renderer/media/midi_message_filter_unittest.cc',
'renderer/media/mock_audio_device_factory.cc',
Expand Down
89 changes: 89 additions & 0 deletions content/renderer/media/html_audio_element_capturer_source.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2016 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 "content/renderer/media/html_audio_element_capturer_source.h"

#include "base/threading/thread_task_runner_handle.h"
#include "media/base/audio_parameters.h"
#include "media/base/audio_renderer_sink.h"
#include "media/blink/webaudiosourceprovider_impl.h"
#include "media/blink/webmediaplayer_impl.h"
#include "third_party/WebKit/public/platform/WebMediaPlayer.h"

namespace content {

//static
HtmlAudioElementCapturerSource*
HtmlAudioElementCapturerSource::CreateFromWebMediaPlayerImpl(
blink::WebMediaPlayer* player) {
DCHECK(player);
return new HtmlAudioElementCapturerSource(
static_cast<media::WebAudioSourceProviderImpl*>(
player->getAudioSourceProvider()));
}

HtmlAudioElementCapturerSource::HtmlAudioElementCapturerSource(
media::WebAudioSourceProviderImpl* audio_source)
: MediaStreamAudioSource(true /* is_local_source */),
audio_source_(audio_source),
is_started_(false),
last_sample_rate_(0),
last_num_channels_(0),
last_bus_frames_(0) {
DCHECK(audio_source_);
}

HtmlAudioElementCapturerSource::~HtmlAudioElementCapturerSource() {
DCHECK(thread_checker_.CalledOnValidThread());
EnsureSourceIsStopped();
}

bool HtmlAudioElementCapturerSource::EnsureSourceIsStarted() {
DCHECK(thread_checker_.CalledOnValidThread());
if (audio_source_ && !is_started_) {
// base:Unretained() is safe here since EnsureSourceIsStopped() guarantees
// no more calls to OnAudioBus().
audio_source_->SetCopyAudioCallback(base::Bind(
&HtmlAudioElementCapturerSource::OnAudioBus, base::Unretained(this)));
is_started_ = true;
}
return is_started_;
}

void HtmlAudioElementCapturerSource::EnsureSourceIsStopped() {
DCHECK(thread_checker_.CalledOnValidThread());
if (!is_started_)
return;

if (audio_source_) {
audio_source_->ClearCopyAudioCallback();
audio_source_ = nullptr;
}
is_started_ = false;
}

void HtmlAudioElementCapturerSource::OnAudioBus(
std::unique_ptr<media::AudioBus> audio_bus,
uint32_t delay_milliseconds,
int sample_rate) {
const base::TimeTicks capture_time =
base::TimeTicks::Now() -
base::TimeDelta::FromMilliseconds(delay_milliseconds);

if (sample_rate != last_sample_rate_ ||
audio_bus->channels() != last_num_channels_ ||
audio_bus->frames() != last_bus_frames_) {
MediaStreamAudioSource::SetFormat(
media::AudioParameters(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
media::GuessChannelLayout(audio_bus->channels()),
sample_rate, 16, audio_bus->frames()));
last_sample_rate_ = sample_rate;
last_num_channels_ = audio_bus->channels();
last_bus_frames_ = audio_bus->frames();
}

MediaStreamAudioSource::DeliverDataToTracks(*audio_bus, capture_time);
}

} // namespace content
63 changes: 63 additions & 0 deletions content/renderer/media/html_audio_element_capturer_source.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2016 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.

#ifndef CONTENT_RENDERER_MEDIA_HTML_AUDIO_ELEMENT_CAPTURER_SOURCE_H_
#define CONTENT_RENDERER_MEDIA_HTML_AUDIO_ELEMENT_CAPTURER_SOURCE_H_

#include "base/callback.h"
#include "base/memory/weak_ptr.h"
#include "base/threading/thread_checker.h"
#include "content/common/content_export.h"
#include "content/renderer/media/media_stream_audio_source.h"

namespace blink {
class WebMediaPlayer;
} // namespace blink

namespace media {
class AudioBus;
class WebAudioSourceProviderImpl;
} // namespace media

namespace content {

// This class is a MediaStreamAudioSource that registers to the constructor-
// passed weak WebAudioSourceProviderImpl to receive a copy of the audio data
// intended for rendering. This copied data is received on OnAudioBus() and sent
// to all the registered Tracks.
class CONTENT_EXPORT HtmlAudioElementCapturerSource final
: NON_EXPORTED_BASE(public MediaStreamAudioSource) {
public:
static HtmlAudioElementCapturerSource*
CreateFromWebMediaPlayerImpl(blink::WebMediaPlayer* player);

explicit HtmlAudioElementCapturerSource(
media::WebAudioSourceProviderImpl* audio_source);
~HtmlAudioElementCapturerSource() override;

private:
// MediaStreamAudioSource implementation.
bool EnsureSourceIsStarted() final;
void EnsureSourceIsStopped() final;

// To act as an WebAudioSourceProviderImpl::CopyAudioCB.
void OnAudioBus(std::unique_ptr<media::AudioBus> audio_bus,
uint32_t delay_milliseconds,
int sample_rate);

scoped_refptr<media::WebAudioSourceProviderImpl> audio_source_;

bool is_started_;
int last_sample_rate_;
int last_num_channels_;
int last_bus_frames_;

base::ThreadChecker thread_checker_;

DISALLOW_COPY_AND_ASSIGN(HtmlAudioElementCapturerSource);
};

} // namespace content

#endif // CONTENT_RENDERER_MEDIA_HTML_AUDIO_ELEMENT_CAPTURER_SOURCE_H_
154 changes: 154 additions & 0 deletions content/renderer/media/html_audio_element_capturer_source_unittest.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Copyright 2016 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/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/threading/thread_task_runner_handle.h"
#include "content/public/renderer/media_stream_audio_sink.h"
#include "content/renderer/media/html_audio_element_capturer_source.h"
#include "content/renderer/media/media_stream_audio_track.h"
#include "media/audio/null_audio_sink.h"
#include "media/base/audio_parameters.h"
#include "media/base/fake_audio_render_callback.h"
#include "media/blink/webaudiosourceprovider_impl.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/WebKit/public/platform/WebString.h"
#include "third_party/WebKit/public/web/WebHeap.h"

using ::testing::_;
using ::testing::AllOf;
using ::testing::InSequence;
using ::testing::Mock;
using ::testing::Property;

namespace content {

static const int kNumChannelsForTest = 1;
static const int kBufferDurationMs = 10;

static const int kAudioTrackSampleRate = 48000;
static const int kAudioTrackSamplesPerBuffer =
kAudioTrackSampleRate * kBufferDurationMs /
base::Time::kMillisecondsPerSecond;

ACTION_P(RunClosure, closure) {
closure.Run();
}

//
class MockMediaStreamAudioSink final : public MediaStreamAudioSink {
public:
MockMediaStreamAudioSink() : MediaStreamAudioSink() {}
~MockMediaStreamAudioSink() = default;

MOCK_METHOD1(OnSetFormat, void(const media::AudioParameters& params));
MOCK_METHOD2(OnData,
void(const media::AudioBus& audio_bus,
base::TimeTicks estimated_capture_time));

DISALLOW_COPY_AND_ASSIGN(MockMediaStreamAudioSink);
};

// This test needs to bundle together plenty of objects, namely:
// - a WebAudioSourceProviderImpl, which in turn needs an Audio Sink, in this
// case a NullAudioSink. This is needed to plug HTMLAudioElementCapturerSource
// and inject audio.
// - a WebMediaStreamSource, that owns the HTMLAudioElementCapturerSource under
// test, and a WebMediaStreamAudioTrack, that the class under test needs to
// connect to in order to operate correctly. This class has an inner content
// MediaStreamAudioTrack.
// - finally, a MockMediaStreamAudioSink to observe captured audio frames, and
// that plugs into the former MediaStreamAudioTrack.
class HTMLAudioElementCapturerSourceTest : public testing::Test {
public:
HTMLAudioElementCapturerSourceTest()
: fake_callback_(0.1),
audio_source_(new media::WebAudioSourceProviderImpl(
new media::NullAudioSink(base::ThreadTaskRunnerHandle::Get()))) {}

void SetUp() final {
const media::AudioParameters params(
media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
media::GuessChannelLayout(kNumChannelsForTest),
kAudioTrackSampleRate /* sample_rate */, 16 /* bits_per_sample */,
kAudioTrackSamplesPerBuffer /* frames_per_buffer */);
audio_source_->Initialize(params, &fake_callback_);

blink_audio_source_.initialize(blink::WebString::fromUTF8("audio_id"),
blink::WebMediaStreamSource::TypeAudio,
blink::WebString::fromUTF8("audio_track"),
false /* remote */);
blink_audio_track_.initialize(blink_audio_source_.id(),
blink_audio_source_);

// |blink_audio_source_| takes ownership of HtmlAudioElementCapturerSource.
blink_audio_source_.setExtraData(
new HtmlAudioElementCapturerSource(audio_source_.get()));
ASSERT_TRUE(source()->ConnectToTrack(blink_audio_track_));
}

void TearDown() override {
blink_audio_track_.reset();
blink_audio_source_.reset();
blink::WebHeap::collectAllGarbageForTesting();
}

HtmlAudioElementCapturerSource* source() const {
return static_cast<HtmlAudioElementCapturerSource*>(
MediaStreamAudioSource::From(blink_audio_source_));
}

MediaStreamAudioTrack* track() const {
return MediaStreamAudioTrack::From(blink_audio_track_);
}

int InjectAudio(media::AudioBus* audio_bus) {
return audio_source_->RenderForTesting(audio_bus);
}

protected:
const base::MessageLoop message_loop_;

blink::WebMediaStreamSource blink_audio_source_;
blink::WebMediaStreamTrack blink_audio_track_;

media::FakeAudioRenderCallback fake_callback_;
scoped_refptr<media::WebAudioSourceProviderImpl> audio_source_;
};

// Constructs and destructs all objects. This is a non trivial sequence.
TEST_F(HTMLAudioElementCapturerSourceTest, ConstructAndDestruct) {
}

// This test verifies that Audio can be properly captured when injected in the
// WebAudioSourceProviderImpl.
TEST_F(HTMLAudioElementCapturerSourceTest, CaptureAudio) {
InSequence s;

base::RunLoop run_loop;
base::Closure quit_closure = run_loop.QuitClosure();

MockMediaStreamAudioSink sink;
track()->AddSink(&sink);
EXPECT_CALL(sink, OnSetFormat(_)).Times(1);
EXPECT_CALL(
sink,
OnData(AllOf(Property(&media::AudioBus::channels, kNumChannelsForTest),
Property(&media::AudioBus::frames,
kAudioTrackSamplesPerBuffer)),
_))
.Times(1)
.WillOnce(RunClosure(quit_closure));

std::unique_ptr<media::AudioBus> bus = media::AudioBus::Create(
kNumChannelsForTest, kAudioTrackSamplesPerBuffer);
InjectAudio(bus.get());
run_loop.Run();

track()->Stop();
track()->RemoveSink(&sink);
}

} // namespace content
29 changes: 29 additions & 0 deletions content/renderer/renderer_blink_platform_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/guid.h"
#include "base/lazy_instance.h"
#include "base/location.h"
#include "base/logging.h"
Expand Down Expand Up @@ -61,6 +62,7 @@
#include "content/renderer/gamepad_shared_memory_reader.h"
#include "content/renderer/media/audio_decoder.h"
#include "content/renderer/media/canvas_capture_handler.h"
#include "content/renderer/media/html_audio_element_capturer_source.h"
#include "content/renderer/media/html_video_element_capturer_source.h"
#include "content/renderer/media/image_capture_frame_grabber.h"
#include "content/renderer/media/media_recorder_handler.h"
Expand Down Expand Up @@ -975,6 +977,33 @@ void RendererBlinkPlatformImpl::createHTMLVideoElementCapturer(
#endif
}

void RendererBlinkPlatformImpl::createHTMLAudioElementCapturer(
WebMediaStream* web_media_stream,
WebMediaPlayer* web_media_player) {
DCHECK(web_media_stream);
DCHECK(web_media_player);

blink::WebMediaStreamSource web_media_stream_source;
blink::WebMediaStreamTrack web_media_stream_track;
const WebString track_id = WebString::fromUTF8(base::GenerateGUID());

web_media_stream_source.initialize(track_id,
blink::WebMediaStreamSource::TypeAudio,
track_id,
false /* is_remote */);
web_media_stream_track.initialize(web_media_stream_source);

MediaStreamAudioSource* const media_stream_source =
HtmlAudioElementCapturerSource::CreateFromWebMediaPlayerImpl(
web_media_player);

// Takes ownership of |media_stream_source|.
web_media_stream_source.setExtraData(media_stream_source);

media_stream_source->ConnectToTrack(web_media_stream_track);
web_media_stream->addTrack(web_media_stream_track);
}

//------------------------------------------------------------------------------

WebImageCaptureFrameGrabber*
Expand Down
3 changes: 3 additions & 0 deletions content/renderer/renderer_blink_platform_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ class CONTENT_EXPORT RendererBlinkPlatformImpl : public BlinkPlatformImpl {
void createHTMLVideoElementCapturer(
blink::WebMediaStream* web_media_stream,
blink::WebMediaPlayer* web_media_player) override;
void createHTMLAudioElementCapturer(
blink::WebMediaStream* web_media_stream,
blink::WebMediaPlayer* web_media_player) override;
blink::WebImageCaptureFrameGrabber* createImageCaptureFrameGrabber() override;
blink::WebGraphicsContext3DProvider* createOffscreenGraphicsContext3DProvider(
const blink::Platform::ContextAttributes& attributes,
Expand Down
1 change: 1 addition & 0 deletions content/test/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,7 @@ test("content_unittests") {
".",
"//content")
deps += [
"//media/blink",
"//third_party/libjingle:libjingle_webrtc",
"//third_party/webrtc/base:rtc_base",
"//third_party/webrtc/modules/desktop_capture:primitives",
Expand Down
Loading

0 comments on commit 77d0d44

Please sign in to comment.