diff options
-rw-r--r-- | content/browser/media/media_internals.cc | 287 | ||||
-rw-r--r-- | content/browser/media/media_internals.h | 106 | ||||
-rw-r--r-- | content/browser/media/media_internals_handler.cc | 9 | ||||
-rw-r--r-- | content/browser/media/media_internals_handler.h | 4 | ||||
-rw-r--r-- | content/browser/media/media_internals_unittest.cc | 253 | ||||
-rw-r--r-- | content/browser/renderer_host/media/audio_renderer_host.cc | 31 | ||||
-rw-r--r-- | content/browser/renderer_host/media/audio_renderer_host.h | 3 | ||||
-rw-r--r-- | content/browser/resources/media/client_renderer.js | 140 | ||||
-rw-r--r-- | content/browser/resources/media/main.js | 33 | ||||
-rw-r--r-- | content/browser/resources/media/manager.js | 52 | ||||
-rw-r--r-- | content/browser/resources/media/media_internals.css | 47 | ||||
-rw-r--r-- | content/browser/resources/media/media_internals.html | 26 | ||||
-rw-r--r-- | content/test/data/media/webui/integration_test.html | 46 | ||||
-rw-r--r-- | media/audio/audio_logging.h | 84 |
14 files changed, 630 insertions, 491 deletions
diff --git a/content/browser/media/media_internals.cc b/content/browser/media/media_internals.cc index 22f164d..9f420e8 100644 --- a/content/browser/media/media_internals.cc +++ b/content/browser/media/media_internals.cc @@ -4,7 +4,6 @@ #include "content/browser/media/media_internals.h" -#include "base/memory/scoped_ptr.h" #include "base/strings/string16.h" #include "base/strings/stringprintf.h" #include "content/public/browser/browser_thread.h" @@ -13,120 +12,169 @@ #include "media/base/media_log.h" #include "media/base/media_log_event.h" -namespace content { +namespace { -MediaInternals* MediaInternals::GetInstance() { - return Singleton<MediaInternals>::get(); +static base::LazyInstance<content::MediaInternals>::Leaky g_media_internals = + LAZY_INSTANCE_INITIALIZER; + +string16 SerializeUpdate(const std::string& function, + const base::Value* value) { + return content::WebUI::GetJavascriptCall( + function, std::vector<const base::Value*>(1, value)); } -MediaInternals::~MediaInternals() {} +const char kAudioLogStatusKey[] = "status"; +const char kAudioLogUpdateFunction[] = "media.updateAudioComponent"; -namespace { -std::string FormatAudioStreamName(void* host, int stream_id) { - return base::StringPrintf("audio_streams.%p:%d", host, stream_id); -} -} +} // namespace -void MediaInternals::OnDeleteAudioStream(void* host, int stream_id) { - DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); - DeleteItem(FormatAudioStreamName(host, stream_id)); -} +namespace content { -void MediaInternals::OnSetAudioStreamPlaying( - void* host, int stream_id, bool playing) { - DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); - UpdateAudioStream(host, stream_id, - "playing", new base::FundamentalValue(playing)); -} +class AudioLogImpl : public media::AudioLog { + public: + AudioLogImpl(int owner_id, + media::AudioLogFactory::AudioComponent component, + content::MediaInternals* media_internals); + virtual ~AudioLogImpl(); + + virtual void OnCreated(int component_id, + const media::AudioParameters& params, + const std::string& input_device_id, + const std::string& output_device_id) OVERRIDE; + virtual void OnStarted(int component_id) OVERRIDE; + virtual void OnStopped(int component_id) OVERRIDE; + virtual void OnClosed(int component_id) OVERRIDE; + virtual void OnError(int component_id) OVERRIDE; + virtual void OnSetVolume(int component_id, double volume) OVERRIDE; -void MediaInternals::OnAudioStreamCreated(void* host, - int stream_id, - const media::AudioParameters& params, - const std::string& input_device_id) { - DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + private: + void SendSingleStringUpdate(int component_id, + const std::string& key, + const std::string& value); + void StoreComponentMetadata(int component_id, base::DictionaryValue* dict); + std::string FormatCacheKey(int component_id); - StoreAudioStream(host, - stream_id, - "input_device_id", - Value::CreateStringValue(input_device_id)); + const int owner_id_; + const media::AudioLogFactory::AudioComponent component_; + content::MediaInternals* const media_internals_; - StoreAudioStream( - host, stream_id, "status", Value::CreateStringValue("created")); + DISALLOW_COPY_AND_ASSIGN(AudioLogImpl); +}; - StoreAudioStream( - host, stream_id, "stream_id", Value::CreateIntegerValue(stream_id)); +AudioLogImpl::AudioLogImpl(int owner_id, + media::AudioLogFactory::AudioComponent component, + content::MediaInternals* media_internals) + : owner_id_(owner_id), + component_(component), + media_internals_(media_internals) {} - StoreAudioStream(host, - stream_id, - "input_channels", - Value::CreateIntegerValue(params.input_channels())); +AudioLogImpl::~AudioLogImpl() {} - StoreAudioStream(host, - stream_id, - "frames_per_buffer", - Value::CreateIntegerValue(params.frames_per_buffer())); +void AudioLogImpl::OnCreated(int component_id, + const media::AudioParameters& params, + const std::string& input_device_id, + const std::string& output_device_id) { + base::DictionaryValue dict; + StoreComponentMetadata(component_id, &dict); - StoreAudioStream(host, - stream_id, - "sample_rate", - Value::CreateIntegerValue(params.sample_rate())); + dict.SetString(kAudioLogStatusKey, "created"); + dict.SetString("input_device_id", input_device_id); + dict.SetInteger("input_channels", params.input_channels()); + dict.SetInteger("frames_per_buffer", params.frames_per_buffer()); + dict.SetInteger("sample_rate", params.sample_rate()); + dict.SetString("output_device_id", output_device_id); + dict.SetInteger("output_channels", params.channels()); + dict.SetString("output_channel_layout", + ChannelLayoutToString(params.channel_layout())); - StoreAudioStream(host, - stream_id, - "output_channels", - Value::CreateIntegerValue(params.channels())); + media_internals_->SendUpdateAndCache( + FormatCacheKey(component_id), kAudioLogUpdateFunction, &dict); +} - StoreAudioStream( - host, - stream_id, - "channel_layout", - Value::CreateStringValue(ChannelLayoutToString(params.channel_layout()))); +void AudioLogImpl::OnStarted(int component_id) { + SendSingleStringUpdate(component_id, kAudioLogStatusKey, "started"); +} - SendEverything(); +void AudioLogImpl::OnStopped(int component_id) { + SendSingleStringUpdate(component_id, kAudioLogStatusKey, "stopped"); } -void MediaInternals::OnSetAudioStreamStatus(void* host, - int stream_id, - const std::string& status) { - DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); - UpdateAudioStream(host, stream_id, - "status", new base::StringValue(status)); +void AudioLogImpl::OnClosed(int component_id) { + base::DictionaryValue dict; + StoreComponentMetadata(component_id, &dict); + dict.SetString(kAudioLogStatusKey, "closed"); + media_internals_->SendUpdateAndPurgeCache( + FormatCacheKey(component_id), kAudioLogUpdateFunction, &dict); } -void MediaInternals::OnSetAudioStreamVolume( - void* host, int stream_id, double volume) { - DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); - UpdateAudioStream(host, stream_id, - "volume", new base::FundamentalValue(volume)); +void AudioLogImpl::OnError(int component_id) { + SendSingleStringUpdate(component_id, "error_occurred", "true"); +} + +void AudioLogImpl::OnSetVolume(int component_id, double volume) { + base::DictionaryValue dict; + StoreComponentMetadata(component_id, &dict); + dict.SetDouble("volume", volume); + media_internals_->SendUpdateAndCache( + FormatCacheKey(component_id), kAudioLogUpdateFunction, &dict); +} + +std::string AudioLogImpl::FormatCacheKey(int component_id) { + return base::StringPrintf("%d:%d:%d", owner_id_, component_, component_id); +} + +void AudioLogImpl::SendSingleStringUpdate(int component_id, + const std::string& key, + const std::string& value) { + base::DictionaryValue dict; + StoreComponentMetadata(component_id, &dict); + dict.SetString(key, value); + media_internals_->SendUpdateAndCache( + FormatCacheKey(component_id), kAudioLogUpdateFunction, &dict); } +void AudioLogImpl::StoreComponentMetadata(int component_id, + base::DictionaryValue* dict) { + dict->SetInteger("owner_id", owner_id_); + dict->SetInteger("component_id", component_id); + dict->SetInteger("component_type", component_); +} + +MediaInternals* MediaInternals::GetInstance() { + return g_media_internals.Pointer(); +} + +MediaInternals::MediaInternals() : owner_ids_() {} +MediaInternals::~MediaInternals() {} + void MediaInternals::OnMediaEvents( int render_process_id, const std::vector<media::MediaLogEvent>& events) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); - - // Notify observers that |event| has occured. + // Notify observers that |event| has occurred. for (std::vector<media::MediaLogEvent>::const_iterator event = events.begin(); - event != events.end(); ++event) { + event != events.end(); ++event) { base::DictionaryValue dict; dict.SetInteger("renderer", render_process_id); dict.SetInteger("player", event->id); dict.SetString("type", media::MediaLog::EventTypeToString(event->type)); - int64 ticks = event->time.ToInternalValue(); - double ticks_millis = - ticks / static_cast<double>(base::Time::kMicrosecondsPerMillisecond); - + // TODO(dalecurtis): This is technically not correct. TimeTicks "can't" be + // converted to to a human readable time format. See base/time/time.h. + const double ticks = event->time.ToInternalValue(); + const double ticks_millis = ticks / base::Time::kMicrosecondsPerMillisecond; dict.SetDouble("ticksMillis", ticks_millis); dict.Set("params", event->params.DeepCopy()); - SendUpdate("media.onMediaEvent", &dict); + SendUpdate(SerializeUpdate("media.onMediaEvent", &dict)); } } void MediaInternals::AddUpdateCallback(const UpdateCallback& callback) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); update_callbacks_.push_back(callback); } void MediaInternals::RemoveUpdateCallback(const UpdateCallback& callback) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); for (size_t i = 0; i < update_callbacks_.size(); ++i) { if (update_callbacks_[i].Equals(callback)) { update_callbacks_.erase(update_callbacks_.begin() + i); @@ -137,68 +185,61 @@ void MediaInternals::RemoveUpdateCallback(const UpdateCallback& callback) { } void MediaInternals::SendEverything() { - DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); - SendUpdate("media.onReceiveEverything", &data_); + string16 everything_update; + { + base::AutoLock auto_lock(lock_); + everything_update = SerializeUpdate( + "media.onReceiveEverything", &cached_data_); + } + SendUpdate(everything_update); } -MediaInternals::MediaInternals() { -} +void MediaInternals::SendUpdate(const string16& update) { + // SendUpdate() may be called from any thread, but must run on the IO thread. + // TODO(dalecurtis): This is pretty silly since the update callbacks simply + // forward the calls to the UI thread. We should avoid the extra hop. + if (!BrowserThread::CurrentlyOn(BrowserThread::IO)) { + BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, base::Bind( + &MediaInternals::SendUpdate, base::Unretained(this), update)); + return; + } -void MediaInternals::StoreAudioStream(void* host, - int stream_id, - const std::string& property, - base::Value* value) { - StoreItem(FormatAudioStreamName(host, stream_id), property, value); + for (size_t i = 0; i < update_callbacks_.size(); i++) + update_callbacks_[i].Run(update); } -void MediaInternals::UpdateAudioStream(void* host, - int stream_id, - const std::string& property, - base::Value* value) { - UpdateItem("media.updateAudioStream", - FormatAudioStreamName(host, stream_id), - property, - value); +scoped_ptr<media::AudioLog> MediaInternals::CreateAudioLog( + AudioComponent component) { + base::AutoLock auto_lock(lock_); + return scoped_ptr<media::AudioLog>(new AudioLogImpl( + owner_ids_[component]++, component, this)); } -void MediaInternals::DeleteItem(const std::string& item) { - data_.Remove(item, NULL); - scoped_ptr<base::Value> value(new base::StringValue(item)); - SendUpdate("media.onItemDeleted", value.get()); -} +void MediaInternals::SendUpdateAndCache(const std::string& cache_key, + const std::string& function, + const base::DictionaryValue* value) { + SendUpdate(SerializeUpdate(function, value)); -base::DictionaryValue* MediaInternals::StoreItem(const std::string& id, - const std::string& property, - base::Value* value) { - base::DictionaryValue* item_properties; - if (!data_.GetDictionary(id, &item_properties)) { - item_properties = new base::DictionaryValue(); - data_.Set(id, item_properties); - item_properties->SetString("id", id); + base::AutoLock auto_lock(lock_); + if (!cached_data_.HasKey(cache_key)) { + cached_data_.Set(cache_key, value->DeepCopy()); + return; } - item_properties->Set(property, value); - return item_properties; -} -void MediaInternals::UpdateItem(const std::string& update_fn, - const std::string& id, - const std::string& property, - base::Value* value) { - base::DictionaryValue* item_properties = StoreItem(id, property, value); - SendUpdate(update_fn, item_properties); + base::DictionaryValue* existing_dict = NULL; + CHECK(cached_data_.GetDictionary(cache_key, &existing_dict)); + existing_dict->MergeDictionary(value); } -void MediaInternals::SendUpdate(const std::string& function, - base::Value* value) { - // Only bother serializing the update to JSON if someone is watching. - if (update_callbacks_.empty()) - return; +void MediaInternals::SendUpdateAndPurgeCache( + const std::string& cache_key, + const std::string& function, + const base::DictionaryValue* value) { + SendUpdate(SerializeUpdate(function, value)); - std::vector<const base::Value*> args; - args.push_back(value); - string16 update = WebUI::GetJavascriptCall(function, args); - for (size_t i = 0; i < update_callbacks_.size(); i++) - update_callbacks_[i].Run(update); + base::AutoLock auto_lock(lock_); + scoped_ptr<base::Value> out_value; + CHECK(cached_data_.Remove(cache_key, &out_value)); } } // namespace content diff --git a/content/browser/media/media_internals.h b/content/browser/media/media_internals.h index 24b314e..8a179e0 100644 --- a/content/browser/media/media_internals.h +++ b/content/browser/media/media_internals.h @@ -8,12 +8,14 @@ #include <string> #include <vector> -#include "base/memory/ref_counted.h" -#include "base/memory/singleton.h" +#include "base/callback_forward.h" +#include "base/compiler_specific.h" +#include "base/lazy_instance.h" #include "base/strings/string16.h" +#include "base/synchronization/lock.h" #include "base/values.h" #include "content/common/content_export.h" -#include "content/public/common/media_stream_request.h" +#include "media/audio/audio_logging.h" namespace media { class AudioParameters; @@ -23,90 +25,60 @@ struct MediaLogEvent; namespace content { // This class stores information about currently active media. -// It's constructed on the UI thread but all of its methods are called on the IO -// thread. -class CONTENT_EXPORT MediaInternals { +class CONTENT_EXPORT MediaInternals + : NON_EXPORTED_BASE(public media::AudioLogFactory) { public: - virtual ~MediaInternals(); - static MediaInternals* GetInstance(); - // The following methods are virtual for gmock. - - // Called when an audio stream is deleted. - virtual void OnDeleteAudioStream(void* host, int stream_id); - - // Called when an audio stream is set to playing or paused. - virtual void OnSetAudioStreamPlaying(void* host, int stream_id, bool playing); - - // Called when an audio stream is created with the parameters that - // it attempts to use to make the stream. - virtual void OnAudioStreamCreated(void* host, - int stream_id, - const media::AudioParameters& params, - const std::string& input_device_id); - - // Called when the status of an audio stream is set to "created", "closed", or - // "error". - virtual void OnSetAudioStreamStatus(void* host, int stream_id, - const std::string& status); - - // Called when the volume of an audio stream is set. - virtual void OnSetAudioStreamVolume(void* host, int stream_id, - double volume); + virtual ~MediaInternals(); // Called when a MediaEvent occurs. - virtual void OnMediaEvents(int render_process_id, - const std::vector<media::MediaLogEvent>& events); + void OnMediaEvents(int render_process_id, + const std::vector<media::MediaLogEvent>& events); // Called with the update string. typedef base::Callback<void(const string16&)> UpdateCallback; - // Add/remove update callbacks (see above). + // Add/remove update callbacks (see above). Must be called on the IO thread. void AddUpdateCallback(const UpdateCallback& callback); void RemoveUpdateCallback(const UpdateCallback& callback); + + // Sends all cached data to each registered UpdateCallback. void SendEverything(); + // AudioLogFactory implementation. Safe to call from any thread. + virtual scoped_ptr<media::AudioLog> CreateAudioLog( + AudioComponent component) OVERRIDE; + private: - friend class MockMediaInternals; + friend class AudioLogImpl; friend class MediaInternalsTest; - friend struct DefaultSingletonTraits<MediaInternals>; + friend struct base::DefaultLazyInstanceTraits<MediaInternals>; MediaInternals(); - // Sets |property| of an audio stream to |value| and notifies observers. - // (host, stream_id) is a unique id for the audio stream. - // |host| will never be dereferenced. - void UpdateAudioStream(void* host, int stream_id, - const std::string& property, base::Value* value); - - // See UpdateAudioStream. The difference is that StoreAudioStream does not - // immediately send the data to the client, instead it waits until - // SendEverything is called. - void StoreAudioStream(void* host, - int stream_id, - const std::string& property, - base::Value* value); - - // Removes |item| from |data_|. - void DeleteItem(const std::string& item); - - base::DictionaryValue* StoreItem(const std::string& id, - const std::string& property, - base::Value* value); - - // Sets data_.id.property = value and notifies attached UIs using update_fn. - // id may be any depth, e.g. "video.decoders.1.2.3" - void UpdateItem(const std::string& update_fn, const std::string& id, - const std::string& property, base::Value* value); - - // Calls javascript |function|(|value|) on each attached UI. - void SendUpdate(const std::string& function, base::Value* value); - - base::DictionaryValue data_; - + // Sends |update| to each registered UpdateCallback. Safe to call from any + // thread, but will forward to the IO thread. + void SendUpdate(const string16& update); + + // Caches |value| under |cache_key| so that future SendEverything() calls will + // include the current data. Calls JavaScript |function|(|value|) for each + // registered UpdateCallback. SendUpdateAndPurgeCache() is similar but purges + // the cache entry after completion instead. + void SendUpdateAndCache(const std::string& cache_key, + const std::string& function, + const base::DictionaryValue* value); + void SendUpdateAndPurgeCache(const std::string& cache_key, + const std::string& function, + const base::DictionaryValue* value); + // Must only be accessed on the IO thread. std::vector<UpdateCallback> update_callbacks_; + // All variables below must be accessed under |lock_|. + base::Lock lock_; + base::DictionaryValue cached_data_; + int owner_ids_[AUDIO_COMPONENT_MAX]; + DISALLOW_COPY_AND_ASSIGN(MediaInternals); }; diff --git a/content/browser/media/media_internals_handler.cc b/content/browser/media/media_internals_handler.cc index 1715330..a45dd4f 100644 --- a/content/browser/media/media_internals_handler.cc +++ b/content/browser/media/media_internals_handler.cc @@ -16,7 +16,8 @@ namespace content { MediaInternalsMessageHandler::MediaInternalsMessageHandler() - : proxy_(new MediaInternalsProxy()) {} + : proxy_(new MediaInternalsProxy()), + page_load_complete_(false) {} MediaInternalsMessageHandler::~MediaInternalsMessageHandler() { proxy_->Detach(); @@ -33,13 +34,15 @@ void MediaInternalsMessageHandler::RegisterMessages() { void MediaInternalsMessageHandler::OnGetEverything( const base::ListValue* list) { + page_load_complete_ = true; proxy_->GetEverything(); } void MediaInternalsMessageHandler::OnUpdate(const string16& update) { - // Don't try to execute JavaScript in a RenderView that no longer exists. + // Don't try to execute JavaScript in a RenderView that no longer exists nor + // if the chrome://media-internals page hasn't finished loading. RenderViewHost* host = web_ui()->GetWebContents()->GetRenderViewHost(); - if (host) + if (host && page_load_complete_) host->ExecuteJavascriptInWebFrame(string16(), update); } diff --git a/content/browser/media/media_internals_handler.h b/content/browser/media/media_internals_handler.h index 4833aee..8ea6f88 100644 --- a/content/browser/media/media_internals_handler.h +++ b/content/browser/media/media_internals_handler.h @@ -35,6 +35,10 @@ class MediaInternalsMessageHandler : public WebUIMessageHandler { private: scoped_refptr<MediaInternalsProxy> proxy_; + // Reflects whether the chrome://media-internals HTML+JS has finished loading. + // If not, it's not safe to send JavaScript calls targeting the page yet. + bool page_load_complete_; + DISALLOW_COPY_AND_ASSIGN(MediaInternalsMessageHandler); }; diff --git a/content/browser/media/media_internals_unittest.cc b/content/browser/media/media_internals_unittest.cc index 173ba36..f5e0375 100644 --- a/content/browser/media/media_internals_unittest.cc +++ b/content/browser/media/media_internals_unittest.cc @@ -5,173 +5,142 @@ #include "content/browser/media/media_internals.h" #include "base/bind.h" -#include "base/memory/scoped_ptr.h" -#include "base/message_loop/message_loop.h" -#include "base/strings/stringprintf.h" -#include "content/public/test/test_browser_thread.h" +#include "base/bind_helpers.h" +#include "base/json/json_reader.h" +#include "base/run_loop.h" +#include "base/strings/utf_string_conversions.h" +#include "content/public/test/test_browser_thread_bundle.h" #include "media/audio/audio_parameters.h" #include "media/base/channel_layout.h" -#include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" -namespace content { namespace { +const int kTestComponentID = 0; +const char kTestInputDeviceID[] = "test-input-id"; +const char kTestOutputDeviceID[] = "test-output-id"; +} // namespace -class MockObserverBaseClass { - public: - ~MockObserverBaseClass() {} - virtual void OnUpdate(const string16& javascript) = 0; -}; +namespace content { -class MockMediaInternalsObserver : public MockObserverBaseClass { +class MediaInternalsTest + : public testing::TestWithParam<media::AudioLogFactory::AudioComponent> { public: - virtual ~MockMediaInternalsObserver() {} - MOCK_METHOD1(OnUpdate, void(const string16& javascript)); -}; - -} // namespace + MediaInternalsTest() + : media_internals_(MediaInternals::GetInstance()), + update_cb_(base::Bind(&MediaInternalsTest::UpdateCallbackImpl, + base::Unretained(this))), + test_params_(media::AudioParameters::AUDIO_PCM_LINEAR, + media::CHANNEL_LAYOUT_MONO, + 48000, + 16, + 128), + test_component_(GetParam()), + audio_log_(media_internals_->CreateAudioLog(test_component_)) { + media_internals_->AddUpdateCallback(update_cb_); + } -class MediaInternalsTest : public testing::Test { - public: - MediaInternalsTest() : io_thread_(BrowserThread::IO, &loop_) {} - base::DictionaryValue* data() { - return &internals_->data_; + virtual ~MediaInternalsTest() { + media_internals_->RemoveUpdateCallback(update_cb_); } - void DeleteItem(const std::string& item) { - internals_->DeleteItem(item); + protected: + // Extracts and deserializes the JSON update data; merges into |update_data_|. + void UpdateCallbackImpl(const string16& update) { + // Each update string looks like "<JavaScript Function Name>({<JSON>});", to + // use the JSON reader we need to strip out the JavaScript code. + std::string utf8_update = base::UTF16ToUTF8(update); + const std::string::size_type first_brace = utf8_update.find('{'); + const std::string::size_type last_brace = utf8_update.rfind('}'); + scoped_ptr<base::Value> output_value(base::JSONReader::Read( + utf8_update.substr(first_brace, last_brace - first_brace + 1))); + CHECK(output_value); + + base::DictionaryValue* output_dict = NULL; + CHECK(output_value->GetAsDictionary(&output_dict)); + update_data_.MergeDictionary(output_dict); } - void UpdateItem(const std::string& item, const std::string& property, - base::Value* value) { - internals_->UpdateItem(std::string(), item, property, value); + void ExpectInt(const std::string& key, int expected_value) { + int actual_value = 0; + ASSERT_TRUE(update_data_.GetInteger(key, &actual_value)); + EXPECT_EQ(expected_value, actual_value); } - void SendUpdate(const std::string& function, base::Value* value) { - internals_->SendUpdate(function, value); + void ExpectString(const std::string& key, const std::string& expected_value) { + std::string actual_value; + ASSERT_TRUE(update_data_.GetString(key, &actual_value)); + EXPECT_EQ(expected_value, actual_value); } - protected: - virtual void SetUp() { - internals_.reset(new MediaInternals()); + void ExpectStatus(const std::string& expected_value) { + ExpectString("status", expected_value); } - base::MessageLoop loop_; - TestBrowserThread io_thread_; - scoped_ptr<MediaInternals> internals_; + TestBrowserThreadBundle thread_bundle_; + MediaInternals* const media_internals_; + MediaInternals::UpdateCallback update_cb_; + base::DictionaryValue update_data_; + const media::AudioParameters test_params_; + const media::AudioLogFactory::AudioComponent test_component_; + scoped_ptr<media::AudioLog> audio_log_; }; -TEST_F(MediaInternalsTest, AudioStreamCreatedSendsMessage) { - media::AudioParameters params = - media::AudioParameters(media::AudioParameters::AUDIO_PCM_LINEAR, - media::CHANNEL_LAYOUT_MONO, - 48000, - 16, - 129); - - const int stream_id = 0; - const std::string device_id = "test"; - const std::string name = - base::StringPrintf("audio_streams.%p:%d", this, stream_id); - - internals_->OnAudioStreamCreated(this, stream_id, params, device_id); - - std::string channel_layout; - data()->GetString(name + ".channel_layout", &channel_layout); - EXPECT_EQ("MONO", channel_layout); - - int sample_rate; - data()->GetInteger(name + ".sample_rate", &sample_rate); - EXPECT_EQ(params.sample_rate(), sample_rate); - - int frames_per_buffer; - data()->GetInteger(name + ".frames_per_buffer", &frames_per_buffer); - EXPECT_EQ(params.frames_per_buffer(), frames_per_buffer); - - int output_channels; - data()->GetInteger(name + ".output_channels", &output_channels); - EXPECT_EQ(params.channels(), output_channels); - - std::string device_id_out; - data()->GetString(name + ".input_device_id", &device_id_out); - EXPECT_EQ(device_id, device_id_out); - - int input_channels; - data()->GetInteger(name + ".input_channels", &input_channels); - EXPECT_EQ(params.input_channels(), input_channels); +TEST_P(MediaInternalsTest, AudioLogCreateStartStopErrorClose) { + audio_log_->OnCreated( + kTestComponentID, test_params_, kTestInputDeviceID, kTestOutputDeviceID); + base::RunLoop().RunUntilIdle(); + + ExpectString("output_channel_layout", + media::ChannelLayoutToString(test_params_.channel_layout())); + ExpectInt("sample_rate", test_params_.sample_rate()); + ExpectInt("frames_per_buffer", test_params_.frames_per_buffer()); + ExpectInt("output_channels", test_params_.channels()); + ExpectInt("input_channels", test_params_.input_channels()); + ExpectString("output_device_id", kTestOutputDeviceID); + ExpectString("input_device_id", kTestInputDeviceID); + ExpectInt("component_id", kTestComponentID); + ExpectInt("component_type", test_component_); + ExpectStatus("created"); + + // Verify OnStarted(). + audio_log_->OnStarted(kTestComponentID); + base::RunLoop().RunUntilIdle(); + ExpectStatus("started"); + + // Verify OnStopped(). + audio_log_->OnStopped(kTestComponentID); + base::RunLoop().RunUntilIdle(); + ExpectStatus("stopped"); + + // Verify OnError(). + const char kErrorKey[] = "error_occurred"; + std::string no_value; + ASSERT_FALSE(update_data_.GetString(kErrorKey, &no_value)); + audio_log_->OnError(kTestComponentID); + base::RunLoop().RunUntilIdle(); + ExpectString(kErrorKey, "true"); + + // Verify OnClosed(). + audio_log_->OnClosed(kTestComponentID); + base::RunLoop().RunUntilIdle(); + ExpectStatus("closed"); } -TEST_F(MediaInternalsTest, UpdateAddsNewItem) { - UpdateItem("some.item", "testing", new base::FundamentalValue(true)); - bool testing = false; - std::string id; +TEST_P(MediaInternalsTest, AudioLogCreateClose) { + audio_log_->OnCreated( + kTestComponentID, test_params_, kTestInputDeviceID, kTestOutputDeviceID); + base::RunLoop().RunUntilIdle(); + ExpectStatus("created"); - EXPECT_TRUE(data()->GetBoolean("some.item.testing", &testing)); - EXPECT_TRUE(testing); - - EXPECT_TRUE(data()->GetString("some.item.id", &id)); - EXPECT_EQ(id, "some.item"); -} - -TEST_F(MediaInternalsTest, UpdateModifiesExistingItem) { - UpdateItem("some.item", "testing", new base::FundamentalValue(true)); - UpdateItem("some.item", "value", new base::FundamentalValue(5)); - UpdateItem("some.item", "testing", new base::FundamentalValue(false)); - bool testing = true; - int value = 0; - std::string id; - - EXPECT_TRUE(data()->GetBoolean("some.item.testing", &testing)); - EXPECT_FALSE(testing); - - EXPECT_TRUE(data()->GetInteger("some.item.value", &value)); - EXPECT_EQ(value, 5); - - EXPECT_TRUE(data()->GetString("some.item.id", &id)); - EXPECT_EQ(id, "some.item"); -} - -TEST_F(MediaInternalsTest, ObserversReceiveNotifications) { - scoped_ptr<MockMediaInternalsObserver> observer( - new MockMediaInternalsObserver()); - - EXPECT_CALL(*observer.get(), OnUpdate(testing::_)).Times(1); - - MediaInternals::UpdateCallback callback = base::Bind( - &MockMediaInternalsObserver::OnUpdate, base::Unretained(observer.get())); - - internals_->AddUpdateCallback(callback); - SendUpdate("fn", data()); -} - -TEST_F(MediaInternalsTest, RemovedObserversReceiveNoNotifications) { - scoped_ptr<MockMediaInternalsObserver> observer( - new MockMediaInternalsObserver()); - - EXPECT_CALL(*observer.get(), OnUpdate(testing::_)).Times(0); - - MediaInternals::UpdateCallback callback = base::Bind( - &MockMediaInternalsObserver::OnUpdate, base::Unretained(observer.get())); - - internals_->AddUpdateCallback(callback); - internals_->RemoveUpdateCallback(callback); - SendUpdate("fn", data()); + audio_log_->OnClosed(kTestComponentID); + base::RunLoop().RunUntilIdle(); + ExpectStatus("closed"); } -TEST_F(MediaInternalsTest, DeleteRemovesItem) { - base::Value* out; - - UpdateItem("some.item", "testing", base::Value::CreateNullValue()); - EXPECT_TRUE(data()->Get("some.item", &out)); - EXPECT_TRUE(data()->Get("some", &out)); - - DeleteItem("some.item"); - EXPECT_FALSE(data()->Get("some.item", &out)); - EXPECT_TRUE(data()->Get("some", &out)); - - DeleteItem("some"); - EXPECT_FALSE(data()->Get("some.item", &out)); - EXPECT_FALSE(data()->Get("some", &out)); -} +INSTANTIATE_TEST_CASE_P( + MediaInternalsTest, MediaInternalsTest, testing::Values( + media::AudioLogFactory::AUDIO_INPUT_CONTROLLER, + media::AudioLogFactory::AUDIO_OUTPUT_CONTROLLER, + media::AudioLogFactory::AUDIO_OUTPUT_STREAM)); } // namespace content diff --git a/content/browser/renderer_host/media/audio_renderer_host.cc b/content/browser/renderer_host/media/audio_renderer_host.cc index 7e6ab82..6282a13 100644 --- a/content/browser/renderer_host/media/audio_renderer_host.cc +++ b/content/browser/renderer_host/media/audio_renderer_host.cc @@ -117,7 +117,8 @@ AudioRendererHost::AudioRendererHost( : render_process_id_(render_process_id), audio_manager_(audio_manager), mirroring_manager_(mirroring_manager), - media_internals_(media_internals), + audio_log_(media_internals->CreateAudioLog( + media::AudioLogFactory::AUDIO_OUTPUT_CONTROLLER)), media_stream_manager_(media_stream_manager) { DCHECK(audio_manager_); DCHECK(media_stream_manager_); @@ -379,10 +380,7 @@ void AudioRendererHost::OnCreateStream( render_process_id_, entry->render_view_id(), entry->controller()); } audio_entries_.insert(std::make_pair(stream_id, entry.release())); - if (media_internals_) { - media_internals_->OnAudioStreamCreated( - this, stream_id, params, input_device_id); - } + audio_log_->OnCreated(stream_id, params, input_device_id, output_device_id); } void AudioRendererHost::OnPlayStream(int stream_id) { @@ -395,8 +393,7 @@ void AudioRendererHost::OnPlayStream(int stream_id) { } entry->controller()->Play(); - if (media_internals_) - media_internals_->OnSetAudioStreamPlaying(this, stream_id, true); + audio_log_->OnStarted(stream_id); } void AudioRendererHost::OnPauseStream(int stream_id) { @@ -409,8 +406,7 @@ void AudioRendererHost::OnPauseStream(int stream_id) { } entry->controller()->Pause(); - if (media_internals_) - media_internals_->OnSetAudioStreamPlaying(this, stream_id, false); + audio_log_->OnStopped(stream_id); } void AudioRendererHost::OnSetVolume(int stream_id, double volume) { @@ -426,8 +422,7 @@ void AudioRendererHost::OnSetVolume(int stream_id, double volume) { if (volume < 0 || volume > 1.0) return; entry->controller()->SetVolume(volume); - if (media_internals_) - media_internals_->OnSetAudioStreamVolume(this, stream_id, volume); + audio_log_->OnSetVolume(stream_id, volume); } void AudioRendererHost::SendErrorMessage(int stream_id) { @@ -453,9 +448,7 @@ void AudioRendererHost::OnCloseStream(int stream_id) { } controller->Close( base::Bind(&AudioRendererHost::DeleteEntry, this, base::Passed(&entry))); - - if (media_internals_) - media_internals_->OnSetAudioStreamStatus(this, stream_id, "closed"); + audio_log_->OnClosed(stream_id); } void AudioRendererHost::DeleteEntry(scoped_ptr<AudioEntry> entry) { @@ -469,12 +462,6 @@ void AudioRendererHost::DeleteEntry(scoped_ptr<AudioEntry> entry) { render_process_id_, entry->render_view_id(), entry->stream_id(), false, -std::numeric_limits<float>::infinity(), false); } - - // Notify the media observer. - if (media_internals_) - media_internals_->OnDeleteAudioStream(this, entry->stream_id()); - - // Note: |entry| will be deleted upon leaving this scope. } void AudioRendererHost::ReportErrorAndClose(int stream_id) { @@ -488,9 +475,7 @@ void AudioRendererHost::ReportErrorAndClose(int stream_id) { SendErrorMessage(stream_id); - if (media_internals_) - media_internals_->OnSetAudioStreamStatus(this, stream_id, "error"); - + audio_log_->OnError(stream_id); OnCloseStream(stream_id); } diff --git a/content/browser/renderer_host/media/audio_renderer_host.h b/content/browser/renderer_host/media/audio_renderer_host.h index cd8b136..1474366 100644 --- a/content/browser/renderer_host/media/audio_renderer_host.h +++ b/content/browser/renderer_host/media/audio_renderer_host.h @@ -49,6 +49,7 @@ #include "content/public/browser/browser_thread.h" #include "content/public/browser/render_view_host.h" #include "media/audio/audio_io.h" +#include "media/audio/audio_logging.h" #include "media/audio/audio_output_controller.h" #include "media/audio/simple_sources.h" @@ -156,7 +157,7 @@ class CONTENT_EXPORT AudioRendererHost : public BrowserMessageFilter { media::AudioManager* const audio_manager_; AudioMirroringManager* const mirroring_manager_; - MediaInternals* const media_internals_; + scoped_ptr<media::AudioLog> audio_log_; // Used to access to AudioInputDeviceManager. MediaStreamManager* media_stream_manager_; diff --git a/content/browser/resources/media/client_renderer.js b/content/browser/resources/media/client_renderer.js index 97acba2..0db66c3 100644 --- a/content/browser/resources/media/client_renderer.js +++ b/content/browser/resources/media/client_renderer.js @@ -5,13 +5,15 @@ var ClientRenderer = (function() { var ClientRenderer = function() { this.playerListElement = document.getElementById('player-list'); - this.audioStreamListElement = document.getElementById('audio-stream-list'); - this.propertiesTable = document.getElementById('property-table'); - this.logTable = document.getElementById('log'); + this.propertiesTable = + document.getElementById('property-table').querySelector('tbody'); + this.logTable = document.getElementById('log').querySelector('tbody'); this.graphElement = document.getElementById('graphs'); this.selectedPlayer = null; - this.selectedStream = null; + this.selectedAudioComponentType = null; + this.selectedAudioComponentId = null; + this.selectedAudioCompontentData = null; this.selectedPlayerLogIndex = 0; @@ -26,6 +28,8 @@ var ClientRenderer = (function() { this.clipboardTextarea = document.getElementById('clipboard-textarea'); this.clipboardButton = document.getElementById('copy-button'); this.clipboardButton.onclick = this.copyToClipboard_.bind(this); + + this.hiddenKeys = ['component_id', 'component_type', 'owner_id']; }; function removeChildren(element) { @@ -46,18 +50,39 @@ var ClientRenderer = (function() { }; ClientRenderer.prototype = { - audioStreamAdded: function(audioStreams, audioStreamAdded) { - this.redrawAudioStreamList_(audioStreams); - }, - - audioStreamUpdated: function(audioStreams, stream, key, value) { - if (stream === this.selectedStream) { - this.drawProperties_(stream); + /** + * Called when an audio component is added to the collection. + * @param componentType Integer AudioComponent enum value; must match values + * from the AudioLogFactory::AudioComponent enum. + * @param components The entire map of components (name -> dict). + */ + audioComponentAdded: function(componentType, components) { + this.redrawAudioComponentList_(componentType, components); + + // Redraw the component if it's currently selected. + if (this.selectedAudioComponentType == componentType && + this.selectedAudioComponentId && + this.selectedAudioComponentId in components) { + this.selectAudioComponent_( + componentType, this.selectedAudioComponentId, + components[this.selectedAudioComponentId]); } }, - audioStreamRemoved: function(audioStreams, audioStreamRemoved) { - this.redrawAudioStreamList_(audioStreams); + /** + * Called when an audio component is removed from the collection. + * @param componentType Integer AudioComponent enum value; must match values + * from the AudioLogFactory::AudioComponent enum. + * @param components The entire map of components (name -> dict). + */ + audioComponentRemoved: function(componentType, components) { + this.redrawAudioComponentList_(componentType, components); + + // Clear the component if it was previously currently selected. + if (this.selectedAudioComponentType == componentType && + !(this.selectedAudioComponentId in components)) { + this.selectAudioComponent_(null, null, {}); + } }, /** @@ -96,48 +121,74 @@ var ClientRenderer = (function() { } }, - redrawAudioStreamList_: function(streams) { - removeChildren(this.audioStreamListElement); + redrawAudioComponentList_: function(componentType, components) { + function redrawList(renderer, baseName, element) { + var fragment = document.createDocumentFragment(); + for (id in components) { + var li = document.createElement('li'); + li.appendChild(createButton( + baseName + ' ' + id, renderer.selectAudioComponent_.bind( + renderer, componentType, id, components[id]))); + fragment.appendChild(li); + } + removeChildren(element); + element.appendChild(fragment); + } - for (id in streams) { - var li = document.createElement('li'); - li.appendChild(createButton( - id, this.selectAudioStream_.bind(this, streams[id]))); - this.audioStreamListElement.appendChild(li); + switch (componentType) { + case 0: + redrawList(this, 'Controller', document.getElementById( + 'audio-input-controller-list')); + break; + case 1: + redrawList(this, 'Controller', document.getElementById( + 'audio-output-controller-list')); + break; + case 2: + redrawList(this, 'Stream', document.getElementById( + 'audio-output-stream-list')); + break; + default: + break; } }, - selectAudioStream_: function(audioStream) { - this.selectedStream = audioStream; + selectAudioComponent_: function(componentType, componentId, componentData) { this.selectedPlayer = null; - this.drawProperties_(audioStream); - removeChildren(this.logTable.querySelector('tbody')); + this.selectedAudioComponentType = componentType; + this.selectedAudioComponentId = componentId; + this.selectedAudioCompontentData = componentData; + this.drawProperties_(componentData); + removeChildren(this.logTable); removeChildren(this.graphElement); }, redrawPlayerList_: function(players) { - removeChildren(this.playerListElement); - + var fragment = document.createDocumentFragment(); for (id in players) { - var li = document.createElement('li'); var player = players[id]; var usableName = player.properties.name || player.properties.url || - 'player ' + player.id; + 'Player ' + player.id; + var li = document.createElement('li'); li.appendChild(createButton( usableName, this.selectPlayer_.bind(this, player))); - this.playerListElement.appendChild(li); + fragment.appendChild(li); } + removeChildren(this.playerListElement); + this.playerListElement.appendChild(fragment); }, selectPlayer_: function(player) { this.selectedPlayer = player; this.selectedPlayerLogIndex = 0; - this.selectedStream = null; + this.selectedAudioComponentType = null; + this.selectedAudioComponentId = null; + this.selectedAudioCompontentData = null; this.drawProperties_(player.properties); - removeChildren(this.logTable.querySelector('tbody')); + removeChildren(this.logTable); removeChildren(this.graphElement); this.drawLog_(); this.drawGraphs_(); @@ -145,12 +196,13 @@ var ClientRenderer = (function() { drawProperties_: function(propertyMap) { removeChildren(this.propertiesTable); - var sortedKeys = Object.keys(propertyMap).sort(); for (var i = 0; i < sortedKeys.length; ++i) { var key = sortedKeys[i]; - var value = propertyMap[key]; + if (this.hiddenKeys.indexOf(key) >= 0) + continue; + var value = propertyMap[key]; var row = this.propertiesTable.insertRow(-1); var keyCell = row.insertCell(-1); var valueCell = row.insertCell(-1); @@ -162,9 +214,11 @@ var ClientRenderer = (function() { appendEventToLog_: function(event) { if (this.filterFunction(event.key)) { - var row = this.logTable.querySelector('tbody').insertRow(-1); + var row = this.logTable.insertRow(-1); - row.insertCell(-1).appendChild(document.createTextNode( + var timestampCell = row.insertCell(-1); + timestampCell.classList.add('timestamp'); + timestampCell.appendChild(document.createTextNode( util.millisecondsToString(event.time))); row.insertCell(-1).appendChild(document.createTextNode(event.key)); row.insertCell(-1).appendChild(document.createTextNode(event.value)); @@ -249,7 +303,7 @@ var ClientRenderer = (function() { }, copyToClipboard_: function() { - var properties = this.selectedStream || + var properties = this.selectedAudioCompontentData || this.selectedPlayer.properties || false; if (!properties) { return; @@ -265,18 +319,14 @@ var ClientRenderer = (function() { } this.clipboardTextarea.value = stringBuffer.join(''); - this.clipboardTextarea.classList.remove('hidden'); + this.clipboardTextarea.classList.remove('hiddenClipboard'); this.clipboardTextarea.focus(); this.clipboardTextarea.select(); - // The act of copying anything from the textarea gets canceled - // if the element in question gets the class 'hidden' (which contains the - // css property display:none) before the event is finished. For this, it - // is necessary put the property setting on the event loop to be executed - // after the copy has taken place. - this.clipboardTextarea.oncopy = function(event) { + // Hide the clipboard element when it loses focus. + this.clipboardTextarea.onblur = function(event) { setTimeout(function(element) { - event.target.classList.add('hidden'); + event.target.classList.add('hiddenClipboard'); }, 0); }; }, @@ -297,7 +347,7 @@ var ClientRenderer = (function() { }; if (this.selectedPlayer) { - removeChildren(this.logTable.querySelector('tbody')); + removeChildren(this.logTable); this.selectedPlayerLogIndex = 0; this.drawLog_(); } diff --git a/content/browser/resources/media/main.js b/content/browser/resources/media/main.js index 14b9918..cb239da 100644 --- a/content/browser/resources/media/main.js +++ b/content/browser/resources/media/main.js @@ -40,8 +40,8 @@ var media = (function() { }; media.onReceiveEverything = function(everything) { - for (var key in everything.audio_streams) { - media.updateAudioStream(everything.audio_streams[key]); + for (var component in everything) { + media.updateAudioComponent(everything[component]); } }; @@ -123,31 +123,20 @@ var media = (function() { }); }; - // For whatever reason, addAudioStream is also called on - // the removal of audio streams. - media.addAudioStream = function(event) { - switch (event.status) { - case 'created': - manager.addAudioStream(event.id); - manager.updateAudioStream(event.id, { 'playing': event.playing }); - break; + media.updateAudioComponent = function(component) { + var uniqueComponentId = component.owner_id + ':' + component.component_id; + switch (component.status) { case 'closed': - manager.removeAudioStream(event.id); + manager.removeAudioComponent( + component.component_type, uniqueComponentId); + break; + default: + manager.updateAudioComponent( + component.component_type, uniqueComponentId, component); break; } }; - media.updateAudioStream = function(stream) { - manager.addAudioStream(stream.id); - manager.updateAudioStream(stream.id, stream); - }; - - media.onItemDeleted = function() { - // This only gets called when an audio stream is removed, which - // for whatever reason is also handled by addAudioStream... - // Because it is already handled, we can safely ignore it. - }; - media.onPlayerOpen = function(id, timestamp) { manager.addPlayer(id, timestamp); }; diff --git a/content/browser/resources/media/manager.js b/content/browser/resources/media/manager.js index 1ba1301..80cd032 100644 --- a/content/browser/resources/media/manager.js +++ b/content/browser/resources/media/manager.js @@ -14,45 +14,47 @@ var Manager = (function() { function Manager(clientRenderer) { this.players_ = {}; - this.audioStreams_ = {}; + this.audioComponents_ = []; this.clientRenderer_ = clientRenderer; } Manager.prototype = { /** - * Adds an audio-stream to the dictionary of audio-streams to manage. - * @param id The unique-id of the audio-stream. - */ - addAudioStream: function(id) { - this.audioStreams_[id] = this.audioStreams_[id] || {}; - this.clientRenderer_.audioStreamAdded(this.audioStreams_, - this.audioStreams_[id]); - }, - - /** - * Sets properties of an audiostream. - * @param id The unique-id of the audio-stream. - * @param properties A dictionary of properties to be added to the - * audio-stream. + * Updates an audio-component. + * @param componentType Integer AudioComponent enum value; must match values + * from the AudioLogFactory::AudioComponent enum. + * @param componentId The unique-id of the audio-component. + * @param componentData The actual component data dictionary. */ - updateAudioStream: function(id, properties) { - for (var key in properties) { - this.audioStreams_[id][key] = properties[key]; + updateAudioComponent: function(componentType, componentId, componentData) { + if (!(componentType in this.audioComponents_)) + this.audioComponents_[componentType] = {}; + if (!(componentId in this.audioComponents_[componentType])) { + this.audioComponents_[componentType][componentId] = componentData; + } else { + for (var key in componentData) { + this.audioComponents_[componentType][componentId][key] = + componentData[key]; + } } - this.clientRenderer_.audioStreamAdded( - this.audioStreams_, this.audioStreams_[id]); + this.clientRenderer_.audioComponentAdded( + componentType, this.audioComponents_[componentType]); }, /** * Removes an audio-stream from the manager. * @param id The unique-id of the audio-stream. */ - removeAudioStream: function(id) { - this.clientRenderer_.audioStreamRemoved( - this.audioStreams_, this.audioStreams_[id]); - delete this.audioStreams_[id]; - }, + removeAudioComponent: function(componentType, componentId) { + if (!(componentType in this.audioComponents_) || + !(componentId in this.audioComponents_[componentType])) { + return; + } + delete this.audioComponents_[componentType][componentId]; + this.clientRenderer_.audioComponentRemoved( + componentType, this.audioComponents_[componentType]); + }, /** * Adds a player to the list of players to manage. diff --git a/content/browser/resources/media/media_internals.css b/content/browser/resources/media/media_internals.css index 9e6ab76..041527f 100644 --- a/content/browser/resources/media/media_internals.css +++ b/content/browser/resources/media/media_internals.css @@ -9,21 +9,30 @@ body, padding: 0; width: 100%; height: 100%; - - font-family: 'Lucida Grande', sans-serif; } table { - border-collapse: collapse; + font-family: sans-serif; + -webkit-font-smoothing: antialiased; + font-size: 115%; + width: auto; + overflow: auto; + display: block; +} +th { + background-color: rgb(112, 196, 105); + font-weight: normal; + color: white; + padding: 2px; + text-align: center; + min-width: 230px; } td { - border: 1px solid black; + background-color: rgb(238, 238, 238); + padding: 2px; + color: rgb(111, 111, 111); word-wrap: break-word; - max-width: 200px; -} -thead { - color: rgb(50,50,50); - font-size: 1.1em; + min-width: 230px; } h1, @@ -56,26 +65,22 @@ h3 { } #player-list-wrapper, -#audio-stream-list-wrapper { +#audio-component-list-wrapper { flex-grow: 1; align-self: stretch; min-width: 200px; - max-width: 200px; overflow: auto; } #player-list-wrapper ul, #player-list-wrapper li, -#audio-stream-list-wrapper ul, -#audio-stream-list-wrapper li { - margin: 0; - padding: 0; +#audio-component-list-wrapper ul, +#audio-component-list-wrapper li { + padding: 0px; list-style-type: none; } #list-wrapper button { - margin: 0; - padding: 0; - width: 170px; + padding: 0px; } #property-wrapper, @@ -103,6 +108,10 @@ h3 { top: 25%; } -.hidden { +.hiddenClipboard { display: none; } + +.timestamp { + min-width: 115px; +}
\ No newline at end of file diff --git a/content/browser/resources/media/media_internals.html b/content/browser/resources/media/media_internals.html index 16d0c55..c555096 100644 --- a/content/browser/resources/media/media_internals.html +++ b/content/browser/resources/media/media_internals.html @@ -13,16 +13,24 @@ found in the LICENSE file. </head> <body> - <textarea id="clipboard-textarea" class="hidden"></textarea> + <textarea id="clipboard-textarea" class="hiddenClipboard"></textarea> <div id="container"> <div id="list-wrapper"> <div id="player-list-wrapper"> <h2>Players</h2> <ul id="player-list"></ul> </div> - <div id="audio-stream-list-wrapper"> - <h2>Audio Streams</h2> - <ul id="audio-stream-list"></ul> + <div id="audio-component-list-wrapper"> + <h2>Input Controllers</h2> + <ul id="audio-input-controller-list"></ul> + </div> + <div id="audio-component-list-wrapper"> + <h2>Output Controllers</h2> + <ul id="audio-output-controller-list"></ul> + </div> + <div id="audio-component-list-wrapper"> + <h2>Output Streams</h2> + <ul id="audio-output-stream-list"></ul> </div> </div> <div id="property-wrapper"> @@ -30,9 +38,11 @@ found in the LICENSE file. <table id="property-table"> <thead> <tr> - <td>Property Name</td> <td>Value</td> + <th>Property</th> + <th>Value</th> </tr> </thead> + <tbody></tbody> </table> <ul id="graphs"></ul> </div> @@ -43,9 +53,9 @@ found in the LICENSE file. <table id="log"> <thead> <tr> - <td>Timestamp</td> - <td>Property</td> - <td>Value</td> + <th class="timestamp">Timestamp</th> + <th>Property</th> + <th>Value</th> </tr> </thead> <tbody></tbody> diff --git a/content/test/data/media/webui/integration_test.html b/content/test/data/media/webui/integration_test.html index be567a0..63aee24 100644 --- a/content/test/data/media/webui/integration_test.html +++ b/content/test/data/media/webui/integration_test.html @@ -15,15 +15,14 @@ found in the LICENSE file. playerUpdated: doNothing, playerRemoved: doNothing, playerAdded: doNothing, - audioStreamUpdated: doNothing, - audioStreamAdded: doNothing, - audioStreamRemoved: doNothing + audioComponentAdded: doNothing, + audioComponentRemoved: doNothing }; var manager = new Manager(mockClientRenderer); media.initialize(manager); - window.manager= manager; + window.manager = manager; }; // The renderer and player ids are completely arbitrarily. @@ -59,19 +58,40 @@ found in the LICENSE file. assertEquals(undefined, window.manager.players_[TEST_NAME]); }; - // Audio Streams are weird, they are handled separately - window.testAddAudioStream = function() { + window.testAudioComponents = function() { var event = { - id: 'ID', - status: 'created', - playing: true + component_id: 1, + component_type: 0, + owner_id: 3, + status: 'created' }; - window.media.addAudioStream(event); + // Ensure no components are currently present. + assertEquals(0, window.manager.audioComponents_.length); - var player = window.manager.audioStreams_[event.id]; - assertTrue(undefined !== player); - assertEquals(event.playing, player['playing']); + // Test adding an audio component. + window.media.updateAudioComponent(event); + assertEquals(1, window.manager.audioComponents_.length); + + // The key format is an implementation detail we don't care about, so + // just ensure there's only one key and then use it directly. + assertEquals(1, Object.keys( + window.manager.audioComponents_[event.component_type]).length); + for (key in window.manager.audioComponents_[event.component_type]) { + var component = + window.manager.audioComponents_[event.component_type][key]; + assertEquals(event.component_id, component['component_id']); + assertEquals(event.component_type, component['component_type']); + assertEquals(event.owner_id, component['owner_id']); + assertEquals(event.status, component['status']); + } + + // Test removing an audio component. + event.status = 'closed'; + window.media.updateAudioComponent(event); + assertEquals(1, window.manager.audioComponents_.length); + assertEquals(0, Object.keys( + window.manager.audioComponents_[event.component_type]).length); }; </script> </body> diff --git a/media/audio/audio_logging.h b/media/audio/audio_logging.h new file mode 100644 index 0000000..1d8366b --- /dev/null +++ b/media/audio/audio_logging.h @@ -0,0 +1,84 @@ +// Copyright 2013 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 MEDIA_AUDIO_AUDIO_LOGGING_H_ +#define MEDIA_AUDIO_AUDIO_LOGGING_H_ + +#include <string> + +#include "base/memory/scoped_ptr.h" + +namespace media { +class AudioParameters; + +// AudioLog logs state information about an active audio component. Each method +// takes a |component_id| along with method specific information. Its methods +// are safe to call from any thread. +class AudioLog { + public: + virtual ~AudioLog() {} + + // Called when an audio component is created. |params| are the parameters of + // the created stream. |input_device_id| and |output_device_id| are the + // respective device ids for input and output. Either one or both may be + // specified. + virtual void OnCreated(int component_id, + const media::AudioParameters& params, + const std::string& input_device_id, + const std::string& output_device_id) = 0; + + // Called when an audio component is started, generally this is synonymous + // with "playing." + virtual void OnStarted(int component_id) = 0; + + // Called when an audio component is stopped, generally this is synonymous + // with "paused." + virtual void OnStopped(int component_id) = 0; + + // Called when an audio component is closed, generally this is synonymous + // with "deleted." + virtual void OnClosed(int component_id) = 0; + + // Called when an audio component encounters an error. + virtual void OnError(int component_id) = 0; + + // Called when an audio component changes volume. |volume| is the new volume. + virtual void OnSetVolume(int component_id, double volume) = 0; +}; + +// AudioLogFactory dispenses AudioLog instances to owning classes for tracking +// AudioComponent behavior. All AudioComponents have the concept of an owning +// class: +// +// - AudioInputRendererHost for AudioInputController +// - AudioRendererHost for AudioOutputController +// - AudioOutputDispatcherImpl for AudioOutputStream +// +// Each of these owning classes may own multiple instances of each component, as +// such each AudioLog supports logging for multiple instances. +class AudioLogFactory { + public: + enum AudioComponent { + // Input controllers have a 1:1 mapping with streams, so there's no need to + // track both controllers and streams. + AUDIO_INPUT_CONTROLLER, + // Output controllers may or may not be backed by an active stream, so we + // need to track both controllers and streams. + AUDIO_OUTPUT_CONTROLLER, + AUDIO_OUTPUT_STREAM, + AUDIO_COMPONENT_MAX + }; + + // Create a new AudioLog object for tracking the behavior for one or more + // instances of the given component. Each instance of an "owning" class must + // create its own AudioLog. + virtual scoped_ptr<AudioLog> CreateAudioLog(AudioComponent component) = 0; + + protected: + virtual ~AudioLogFactory() {} +}; + +} // namespace media + +#endif // MEDIA_AUDIO_AUDIO_LOGGING_H_ |