|  | // Copyright (c) 2012 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/browser/media/media_internals.h" | 
|  |  | 
|  | #include <stddef.h> | 
|  |  | 
|  | #include <tuple> | 
|  | #include <utility> | 
|  |  | 
|  | #include "base/macros.h" | 
|  | #include "base/metrics/histogram_functions.h" | 
|  | #include "base/metrics/histogram_macros.h" | 
|  | #include "base/strings/string16.h" | 
|  | #include "base/strings/string_number_conversions.h" | 
|  | #include "base/strings/stringprintf.h" | 
|  | #include "build/build_config.h" | 
|  | #include "content/browser/media/mojo_audio_logging_adapter.h" | 
|  | #include "content/browser/renderer_host/media/media_stream_manager.h" | 
|  | #include "content/public/browser/browser_context.h" | 
|  | #include "content/public/browser/browser_thread.h" | 
|  | #include "content/public/browser/notification_service.h" | 
|  | #include "content/public/browser/notification_types.h" | 
|  | #include "content/public/browser/render_frame_host.h" | 
|  | #include "content/public/browser/render_process_host.h" | 
|  | #include "content/public/browser/web_contents.h" | 
|  | #include "content/public/browser/web_ui.h" | 
|  | #include "media/base/audio_parameters.h" | 
|  | #include "media/base/media_log_event.h" | 
|  | #include "media/filters/gpu_video_decoder.h" | 
|  | #include "mojo/public/cpp/bindings/strong_binding.h" | 
|  |  | 
|  | #if !defined(OS_ANDROID) | 
|  | #include "media/filters/decrypting_video_decoder.h" | 
|  | #endif | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | base::string16 SerializeUpdate(const std::string& function, | 
|  | const base::Value* value) { | 
|  | return content::WebUI::GetJavascriptCall( | 
|  | function, std::vector<const base::Value*>(1, value)); | 
|  | } | 
|  |  | 
|  | std::string EffectsToString(int effects) { | 
|  | if (effects == media::AudioParameters::NO_EFFECTS) | 
|  | return "NO_EFFECTS"; | 
|  |  | 
|  | struct { | 
|  | int flag; | 
|  | const char* name; | 
|  | } flags[] = { | 
|  | {media::AudioParameters::ECHO_CANCELLER, "ECHO_CANCELLER"}, | 
|  | {media::AudioParameters::DUCKING, "DUCKING"}, | 
|  | {media::AudioParameters::KEYBOARD_MIC, "KEYBOARD_MIC"}, | 
|  | {media::AudioParameters::HOTWORD, "HOTWORD"}, | 
|  | }; | 
|  |  | 
|  | std::string ret; | 
|  | for (size_t i = 0; i < arraysize(flags); ++i) { | 
|  | if (effects & flags[i].flag) { | 
|  | if (!ret.empty()) | 
|  | ret += " | "; | 
|  | ret += flags[i].name; | 
|  | effects &= ~flags[i].flag; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (effects) { | 
|  | if (!ret.empty()) | 
|  | ret += " | "; | 
|  | ret += base::IntToString(effects); | 
|  | } | 
|  |  | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | std::string FormatToString(media::AudioParameters::Format format) { | 
|  | switch (format) { | 
|  | case media::AudioParameters::AUDIO_PCM_LINEAR: | 
|  | return "pcm_linear"; | 
|  | case media::AudioParameters::AUDIO_PCM_LOW_LATENCY: | 
|  | return "pcm_low_latency"; | 
|  | case media::AudioParameters::AUDIO_BITSTREAM_AC3: | 
|  | return "ac3"; | 
|  | case media::AudioParameters::AUDIO_BITSTREAM_EAC3: | 
|  | return "eac3"; | 
|  | case media::AudioParameters::AUDIO_FAKE: | 
|  | return "fake"; | 
|  | } | 
|  |  | 
|  | NOTREACHED(); | 
|  | return "unknown"; | 
|  | } | 
|  |  | 
|  | // Whether the player is in incognito mode or ChromeOS guest mode. | 
|  | bool IsIncognito(int render_process_id) { | 
|  | content::RenderProcessHost* render_process_host = | 
|  | content::RenderProcessHost::FromID(render_process_id); | 
|  | if (!render_process_host) { | 
|  | // This could happen in tests. | 
|  | LOG(ERROR) << "Cannot get RenderProcessHost"; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | content::BrowserContext* browser_context = | 
|  | render_process_host->GetBrowserContext(); | 
|  | DCHECK(browser_context); | 
|  |  | 
|  | return browser_context->IsOffTheRecord(); | 
|  | } | 
|  |  | 
|  | const char kAudioLogStatusKey[] = "status"; | 
|  | const char kAudioLogUpdateFunction[] = "media.updateAudioComponent"; | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | namespace content { | 
|  |  | 
|  | class AudioLogImpl : public media::mojom::AudioLog { | 
|  | public: | 
|  | AudioLogImpl(int owner_id, | 
|  | media::AudioLogFactory::AudioComponent component, | 
|  | content::MediaInternals* media_internals, | 
|  | int component_id, | 
|  | int render_process_id, | 
|  | int render_frame_id); | 
|  | ~AudioLogImpl() override; | 
|  |  | 
|  | void OnCreated(const media::AudioParameters& params, | 
|  | const std::string& device_id) override; | 
|  | void OnStarted() override; | 
|  | void OnStopped() override; | 
|  | void OnClosed() override; | 
|  | void OnError() override; | 
|  | void OnSetVolume(double volume) override; | 
|  | void OnLogMessage(const std::string& message) override; | 
|  |  | 
|  | private: | 
|  | // If possible, i.e. a WebContents exists for the given RenderFrameHostID, | 
|  | // tells an existing AudioLogEntry the WebContents title for easier | 
|  | // differentiation on the UI. Note that the log entry must be created (by | 
|  | // calling OnCreated() before calling this method. | 
|  | void SetWebContentsTitle(); | 
|  |  | 
|  | void SendSingleStringUpdate(const std::string& key, const std::string& value); | 
|  | void StoreComponentMetadata(base::DictionaryValue* dict); | 
|  | std::string FormatCacheKey(); | 
|  |  | 
|  | static void SendWebContentsTitleHelper( | 
|  | const std::string& cache_key, | 
|  | std::unique_ptr<base::DictionaryValue> dict, | 
|  | int render_process_id, | 
|  | int render_frame_id); | 
|  |  | 
|  | const int owner_id_; | 
|  | const media::AudioLogFactory::AudioComponent component_; | 
|  | content::MediaInternals* const media_internals_; | 
|  | const int component_id_; | 
|  | const int render_process_id_; | 
|  | const int render_frame_id_; | 
|  |  | 
|  | DISALLOW_COPY_AND_ASSIGN(AudioLogImpl); | 
|  | }; | 
|  |  | 
|  | AudioLogImpl::AudioLogImpl(int owner_id, | 
|  | media::AudioLogFactory::AudioComponent component, | 
|  | content::MediaInternals* media_internals, | 
|  | int component_id, | 
|  | int render_process_id, | 
|  | int render_frame_id) | 
|  | : owner_id_(owner_id), | 
|  | component_(component), | 
|  | media_internals_(media_internals), | 
|  | component_id_(component_id), | 
|  | render_process_id_(render_process_id), | 
|  | render_frame_id_(render_frame_id) {} | 
|  |  | 
|  | AudioLogImpl::~AudioLogImpl() {} | 
|  |  | 
|  | void AudioLogImpl::OnCreated(const media::AudioParameters& params, | 
|  | const std::string& device_id) { | 
|  | base::DictionaryValue dict; | 
|  | StoreComponentMetadata(&dict); | 
|  |  | 
|  | dict.SetString(kAudioLogStatusKey, "created"); | 
|  | dict.SetString("device_id", device_id); | 
|  | dict.SetString("device_type", FormatToString(params.format())); | 
|  | dict.SetInteger("frames_per_buffer", params.frames_per_buffer()); | 
|  | dict.SetInteger("sample_rate", params.sample_rate()); | 
|  | dict.SetInteger("channels", params.channels()); | 
|  | dict.SetString("channel_layout", | 
|  | ChannelLayoutToString(params.channel_layout())); | 
|  | dict.SetString("effects", EffectsToString(params.effects())); | 
|  |  | 
|  | media_internals_->UpdateAudioLog(MediaInternals::CREATE, FormatCacheKey(), | 
|  | kAudioLogUpdateFunction, &dict); | 
|  | SetWebContentsTitle(); | 
|  | } | 
|  |  | 
|  | void AudioLogImpl::OnStarted() { | 
|  | SendSingleStringUpdate(kAudioLogStatusKey, "started"); | 
|  | } | 
|  |  | 
|  | void AudioLogImpl::OnStopped() { | 
|  | SendSingleStringUpdate(kAudioLogStatusKey, "stopped"); | 
|  | } | 
|  |  | 
|  | void AudioLogImpl::OnClosed() { | 
|  | base::DictionaryValue dict; | 
|  | StoreComponentMetadata(&dict); | 
|  | dict.SetString(kAudioLogStatusKey, "closed"); | 
|  | media_internals_->UpdateAudioLog(MediaInternals::UPDATE_AND_DELETE, | 
|  | FormatCacheKey(), kAudioLogUpdateFunction, | 
|  | &dict); | 
|  | } | 
|  |  | 
|  | void AudioLogImpl::OnError() { | 
|  | SendSingleStringUpdate("error_occurred", "true"); | 
|  | } | 
|  |  | 
|  | void AudioLogImpl::OnSetVolume(double volume) { | 
|  | base::DictionaryValue dict; | 
|  | StoreComponentMetadata(&dict); | 
|  | dict.SetDouble("volume", volume); | 
|  | media_internals_->UpdateAudioLog(MediaInternals::UPDATE_IF_EXISTS, | 
|  | FormatCacheKey(), kAudioLogUpdateFunction, | 
|  | &dict); | 
|  | } | 
|  |  | 
|  | void AudioLogImpl::OnLogMessage(const std::string& message) { | 
|  | MediaStreamManager::SendMessageToNativeLog(message); | 
|  | } | 
|  |  | 
|  | void AudioLogImpl::SetWebContentsTitle() { | 
|  | if (render_process_id_ < 0 || render_frame_id_ < 0) | 
|  | return; | 
|  | std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue()); | 
|  | StoreComponentMetadata(dict.get()); | 
|  | SendWebContentsTitleHelper(FormatCacheKey(), std::move(dict), | 
|  | render_process_id_, render_frame_id_); | 
|  | } | 
|  |  | 
|  | std::string AudioLogImpl::FormatCacheKey() { | 
|  | return base::StringPrintf("%d:%d:%d", owner_id_, component_, component_id_); | 
|  | } | 
|  |  | 
|  | // static | 
|  | void AudioLogImpl::SendWebContentsTitleHelper( | 
|  | const std::string& cache_key, | 
|  | std::unique_ptr<base::DictionaryValue> dict, | 
|  | int render_process_id, | 
|  | int render_frame_id) { | 
|  | // Page title information can only be retrieved from the UI thread. | 
|  | if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) { | 
|  | BrowserThread::PostTask( | 
|  | BrowserThread::UI, FROM_HERE, | 
|  | base::BindOnce(&SendWebContentsTitleHelper, cache_key, std::move(dict), | 
|  | render_process_id, render_frame_id)); | 
|  | return; | 
|  | } | 
|  |  | 
|  | const WebContents* web_contents = WebContents::FromRenderFrameHost( | 
|  | RenderFrameHost::FromID(render_process_id, render_frame_id)); | 
|  | if (!web_contents) | 
|  | return; | 
|  |  | 
|  | // Note: by this point the given audio log entry could have been destroyed, so | 
|  | // we use UPDATE_IF_EXISTS to discard such instances. | 
|  | dict->SetInteger("render_process_id", render_process_id); | 
|  | dict->SetString("web_contents_title", web_contents->GetTitle()); | 
|  | MediaInternals::GetInstance()->UpdateAudioLog( | 
|  | MediaInternals::UPDATE_IF_EXISTS, cache_key, kAudioLogUpdateFunction, | 
|  | dict.get()); | 
|  | } | 
|  |  | 
|  | void AudioLogImpl::SendSingleStringUpdate(const std::string& key, | 
|  | const std::string& value) { | 
|  | base::DictionaryValue dict; | 
|  | StoreComponentMetadata(&dict); | 
|  | dict.SetString(key, value); | 
|  | media_internals_->UpdateAudioLog(MediaInternals::UPDATE_IF_EXISTS, | 
|  | FormatCacheKey(), kAudioLogUpdateFunction, | 
|  | &dict); | 
|  | } | 
|  |  | 
|  | void AudioLogImpl::StoreComponentMetadata(base::DictionaryValue* dict) { | 
|  | dict->SetInteger("owner_id", owner_id_); | 
|  | dict->SetInteger("component_id", component_id_); | 
|  | dict->SetInteger("component_type", component_); | 
|  | } | 
|  |  | 
|  | // This class lives on the browser UI thread. | 
|  | class MediaInternals::MediaInternalsUMAHandler { | 
|  | public: | 
|  | MediaInternalsUMAHandler(); | 
|  |  | 
|  | // Called when a render process is terminated. Reports the pipeline status to | 
|  | // UMA for every player associated with the renderer process and then deletes | 
|  | // the player state. | 
|  | void OnProcessTerminated(int render_process_id); | 
|  |  | 
|  | // Helper function to save the event payload to RendererPlayerMap. | 
|  | void SavePlayerState(int render_process_id, | 
|  | const media::MediaLogEvent& event); | 
|  |  | 
|  | private: | 
|  | struct PipelineInfo { | 
|  | explicit PipelineInfo(bool is_incognito) : is_incognito(is_incognito) {} | 
|  |  | 
|  | bool has_pipeline = false; | 
|  | bool has_ever_played = false; | 
|  | bool has_reached_have_enough = false; | 
|  | media::PipelineStatus last_pipeline_status = media::PIPELINE_OK; | 
|  | bool has_audio = false; | 
|  | bool has_video = false; | 
|  | bool video_dds = false; | 
|  | bool video_decoder_changed = false; | 
|  | bool has_cdm = false; | 
|  | bool is_incognito = false; | 
|  | std::string audio_codec_name; | 
|  | std::string video_codec_name; | 
|  | std::string video_decoder; | 
|  | GURL origin_url; | 
|  | }; | 
|  |  | 
|  | // Helper function to report PipelineStatus associated with a player to UMA. | 
|  | void ReportUMAForPipelineStatus(const PipelineInfo& player_info); | 
|  |  | 
|  | // Helper to generate PipelineStatus UMA name for AudioVideo streams. | 
|  | std::string GetUMANameForAVStream(const PipelineInfo& player_info); | 
|  |  | 
|  | // Key is player id. | 
|  | typedef std::map<int, PipelineInfo> PlayerInfoMap; | 
|  |  | 
|  | // Key is renderer id. | 
|  | typedef std::map<int, PlayerInfoMap> RendererPlayerMap; | 
|  |  | 
|  | // Stores player information per renderer. | 
|  | RendererPlayerMap renderer_info_; | 
|  |  | 
|  | DISALLOW_COPY_AND_ASSIGN(MediaInternalsUMAHandler); | 
|  | }; | 
|  |  | 
|  | MediaInternals::MediaInternalsUMAHandler::MediaInternalsUMAHandler() {} | 
|  |  | 
|  | void MediaInternals::MediaInternalsUMAHandler::SavePlayerState( | 
|  | int render_process_id, | 
|  | const media::MediaLogEvent& event) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  |  | 
|  | PlayerInfoMap& player_info_map = renderer_info_[render_process_id]; | 
|  |  | 
|  | auto it = player_info_map.find(event.id); | 
|  | if (it == player_info_map.end()) { | 
|  | bool success = false; | 
|  | std::tie(it, success) = player_info_map.emplace( | 
|  | std::make_pair(event.id, PipelineInfo(IsIncognito(render_process_id)))); | 
|  | if (!success) { | 
|  | LOG(ERROR) << "Failed to insert a new PipelineInfo."; | 
|  | return; | 
|  | } | 
|  | } | 
|  |  | 
|  | PipelineInfo& player_info = it->second; | 
|  |  | 
|  | switch (event.type) { | 
|  | case media::MediaLogEvent::Type::WEBMEDIAPLAYER_CREATED: { | 
|  | std::string origin_url; | 
|  | event.params.GetString("origin_url", &origin_url); | 
|  | player_info.origin_url = GURL(origin_url); | 
|  | break; | 
|  | } | 
|  | case media::MediaLogEvent::PLAY: { | 
|  | player_info.has_ever_played = true; | 
|  | break; | 
|  | } | 
|  | case media::MediaLogEvent::PIPELINE_STATE_CHANGED: { | 
|  | player_info.has_pipeline = true; | 
|  | break; | 
|  | } | 
|  | case media::MediaLogEvent::PIPELINE_ERROR: { | 
|  | int status = static_cast<media::PipelineStatus>(media::PIPELINE_OK); | 
|  | event.params.GetInteger("pipeline_error", &status); | 
|  | player_info.last_pipeline_status = | 
|  | static_cast<media::PipelineStatus>(status); | 
|  | break; | 
|  | } | 
|  | case media::MediaLogEvent::PROPERTY_CHANGE: | 
|  | if (event.params.HasKey("found_audio_stream")) { | 
|  | event.params.GetBoolean("found_audio_stream", &player_info.has_audio); | 
|  | } | 
|  | if (event.params.HasKey("found_video_stream")) { | 
|  | event.params.GetBoolean("found_video_stream", &player_info.has_video); | 
|  | } | 
|  | if (event.params.HasKey("audio_codec_name")) { | 
|  | event.params.GetString("audio_codec_name", | 
|  | &player_info.audio_codec_name); | 
|  | } | 
|  | if (event.params.HasKey("video_codec_name")) { | 
|  | event.params.GetString("video_codec_name", | 
|  | &player_info.video_codec_name); | 
|  | } | 
|  | if (event.params.HasKey("video_decoder")) { | 
|  | std::string previous_video_decoder(player_info.video_decoder); | 
|  | event.params.GetString("video_decoder", &player_info.video_decoder); | 
|  | if (!previous_video_decoder.empty() && | 
|  | previous_video_decoder != player_info.video_decoder) { | 
|  | player_info.video_decoder_changed = true; | 
|  | } | 
|  | } | 
|  | if (event.params.HasKey("video_dds")) { | 
|  | event.params.GetBoolean("video_dds", &player_info.video_dds); | 
|  | } | 
|  | if (event.params.HasKey("has_cdm")) { | 
|  | event.params.GetBoolean("has_cdm", &player_info.has_cdm); | 
|  | } | 
|  | if (event.params.HasKey("pipeline_buffering_state")) { | 
|  | std::string buffering_state; | 
|  | event.params.GetString("pipeline_buffering_state", &buffering_state); | 
|  |  | 
|  | bool for_suspended_start; | 
|  | event.params.GetBoolean("for_suspended_start", &for_suspended_start); | 
|  |  | 
|  | // Ignore the BUFFERING_HAVE_ENOUGH event if it was for a suspended | 
|  | // start. Otherwise we won't reflect reductions to the HasEverPlayed | 
|  | // statistic. | 
|  | if (buffering_state == "BUFFERING_HAVE_ENOUGH" && !for_suspended_start) | 
|  | player_info.has_reached_have_enough = true; | 
|  | } | 
|  | break; | 
|  | case media::MediaLogEvent::Type::WEBMEDIAPLAYER_DESTROYED: { | 
|  | // Upon player destruction report UMA data; if the player is not torn down | 
|  | // before process exit, it will be logged during OnProcessTerminated(). | 
|  | ReportUMAForPipelineStatus(player_info); | 
|  | player_info_map.erase(it); | 
|  | break; | 
|  | } | 
|  | default: | 
|  | break; | 
|  | } | 
|  | return; | 
|  | } | 
|  |  | 
|  | std::string MediaInternals::MediaInternalsUMAHandler::GetUMANameForAVStream( | 
|  | const PipelineInfo& player_info) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | static const char kPipelineUmaPrefix[] = "Media.PipelineStatus.AudioVideo."; | 
|  | std::string uma_name = kPipelineUmaPrefix; | 
|  | if (player_info.video_codec_name == "vp8") { | 
|  | uma_name += "VP8."; | 
|  | } else if (player_info.video_codec_name == "vp9") { | 
|  | uma_name += "VP9."; | 
|  | } else if (player_info.video_codec_name == "h264") { | 
|  | uma_name += "H264."; | 
|  | } else { | 
|  | return uma_name + "Other"; | 
|  | } | 
|  |  | 
|  | #if !defined(OS_ANDROID) | 
|  | if (player_info.video_decoder == | 
|  | media::DecryptingVideoDecoder::kDecoderName) { | 
|  | return uma_name + "DVD"; | 
|  | } | 
|  | #endif | 
|  |  | 
|  | if (player_info.video_dds) { | 
|  | uma_name += "DDS."; | 
|  | } | 
|  |  | 
|  | if (player_info.video_decoder == media::GpuVideoDecoder::kDecoderName) { | 
|  | uma_name += "HW"; | 
|  | } else { | 
|  | uma_name += "SW"; | 
|  | } | 
|  | return uma_name; | 
|  | } | 
|  |  | 
|  | // TODO(xhwang): This function reports more metrics than just pipeline status | 
|  | // and should be renamed. Similarly, PipelineInfo should be PlayerInfo. | 
|  | void MediaInternals::MediaInternalsUMAHandler::ReportUMAForPipelineStatus( | 
|  | const PipelineInfo& player_info) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  |  | 
|  | // Don't log pipeline status for players which don't actually have a pipeline; | 
|  | // e.g., the Android MediaSourcePlayer implementation. | 
|  | if (!player_info.has_pipeline) | 
|  | return; | 
|  |  | 
|  | if (player_info.has_video && player_info.has_audio) { | 
|  | base::UmaHistogramExactLinear(GetUMANameForAVStream(player_info), | 
|  | player_info.last_pipeline_status, | 
|  | media::PIPELINE_STATUS_MAX); | 
|  | } else if (player_info.has_audio) { | 
|  | UMA_HISTOGRAM_ENUMERATION("Media.PipelineStatus.AudioOnly", | 
|  | player_info.last_pipeline_status, | 
|  | media::PIPELINE_STATUS_MAX + 1); | 
|  | } else if (player_info.has_video) { | 
|  | UMA_HISTOGRAM_ENUMERATION("Media.PipelineStatus.VideoOnly", | 
|  | player_info.last_pipeline_status, | 
|  | media::PIPELINE_STATUS_MAX + 1); | 
|  | } else { | 
|  | // Note: This metric can be recorded as a result of normal operation with | 
|  | // Media Source Extensions. If a site creates a MediaSource object but never | 
|  | // creates a source buffer or appends data, PIPELINE_OK will be recorded. | 
|  | UMA_HISTOGRAM_ENUMERATION("Media.PipelineStatus.Unsupported", | 
|  | player_info.last_pipeline_status, | 
|  | media::PIPELINE_STATUS_MAX + 1); | 
|  | } | 
|  | // Report whether video decoder fallback happened, but only if a video decoder | 
|  | // was reported. | 
|  | if (!player_info.video_decoder.empty()) { | 
|  | UMA_HISTOGRAM_BOOLEAN("Media.VideoDecoderFallback", | 
|  | player_info.video_decoder_changed); | 
|  | } | 
|  |  | 
|  | // Report whether this player ever saw a playback event. Used to measure the | 
|  | // effectiveness of efforts to reduce loaded-but-never-used players. | 
|  | if (player_info.has_reached_have_enough) | 
|  | UMA_HISTOGRAM_BOOLEAN("Media.HasEverPlayed", player_info.has_ever_played); | 
|  |  | 
|  | // Report whether an encrypted playback is in incognito window, excluding | 
|  | // never-used players. | 
|  | if (player_info.has_cdm && player_info.has_ever_played) | 
|  | UMA_HISTOGRAM_BOOLEAN("Media.EME.IsIncognito", player_info.is_incognito); | 
|  | } | 
|  |  | 
|  | void MediaInternals::MediaInternalsUMAHandler::OnProcessTerminated( | 
|  | int render_process_id) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  |  | 
|  | auto players_it = renderer_info_.find(render_process_id); | 
|  | if (players_it == renderer_info_.end()) | 
|  | return; | 
|  | auto it = players_it->second.begin(); | 
|  | while (it != players_it->second.end()) { | 
|  | ReportUMAForPipelineStatus(it->second); | 
|  | players_it->second.erase(it++); | 
|  | } | 
|  | renderer_info_.erase(players_it); | 
|  | } | 
|  |  | 
|  | MediaInternals* MediaInternals::GetInstance() { | 
|  | static content::MediaInternals* internals = new content::MediaInternals(); | 
|  | return internals; | 
|  | } | 
|  |  | 
|  | MediaInternals::MediaInternals() | 
|  | : can_update_(false), | 
|  | owner_ids_(), | 
|  | uma_handler_(new MediaInternalsUMAHandler()) { | 
|  | // TODO(sandersd): Is there ever a relevant case where TERMINATED is sent | 
|  | // without CLOSED also being sent? | 
|  | registrar_.Add(this, NOTIFICATION_RENDERER_PROCESS_CLOSED, | 
|  | NotificationService::AllBrowserContextsAndSources()); | 
|  | registrar_.Add(this, NOTIFICATION_RENDERER_PROCESS_TERMINATED, | 
|  | NotificationService::AllBrowserContextsAndSources()); | 
|  | } | 
|  |  | 
|  | MediaInternals::~MediaInternals() {} | 
|  |  | 
|  | void MediaInternals::Observe(int type, | 
|  | const NotificationSource& source, | 
|  | const NotificationDetails& details) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | RenderProcessHost* process = Source<RenderProcessHost>(source).ptr(); | 
|  | uma_handler_->OnProcessTerminated(process->GetID()); | 
|  | // TODO(sandersd): Send a termination event before clearing the log. | 
|  | saved_events_by_process_.erase(process->GetID()); | 
|  | } | 
|  |  | 
|  | // Converts the |event| to a |update|. Returns whether the conversion succeeded. | 
|  | static bool ConvertEventToUpdate(int render_process_id, | 
|  | const media::MediaLogEvent& event, | 
|  | base::string16* update) { | 
|  | DCHECK(update); | 
|  |  | 
|  | base::DictionaryValue dict; | 
|  | dict.SetInteger("renderer", render_process_id); | 
|  | dict.SetInteger("player", event.id); | 
|  | dict.SetString("type", media::MediaLog::EventTypeToString(event.type)); | 
|  |  | 
|  | // 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); | 
|  |  | 
|  | // Convert PipelineStatus to human readable string | 
|  | if (event.type == media::MediaLogEvent::PIPELINE_ERROR) { | 
|  | int status; | 
|  | if (!event.params.GetInteger("pipeline_error", &status) || | 
|  | status < static_cast<int>(media::PIPELINE_OK) || | 
|  | status > static_cast<int>(media::PIPELINE_STATUS_MAX)) { | 
|  | return false; | 
|  | } | 
|  | media::PipelineStatus error = static_cast<media::PipelineStatus>(status); | 
|  | dict.SetString("params.pipeline_error", | 
|  | media::MediaLog::PipelineStatusToString(error)); | 
|  | } else { | 
|  | dict.SetKey("params", event.params.Clone()); | 
|  | } | 
|  |  | 
|  | *update = SerializeUpdate("media.onMediaEvent", &dict); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | void MediaInternals::OnMediaEvents( | 
|  | int render_process_id, | 
|  | const std::vector<media::MediaLogEvent>& events) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | // Notify observers that |event| has occurred. | 
|  | for (const auto& event : events) { | 
|  | if (CanUpdate()) { | 
|  | base::string16 update; | 
|  | if (ConvertEventToUpdate(render_process_id, event, &update)) | 
|  | SendUpdate(update); | 
|  | } | 
|  | SaveEvent(render_process_id, event); | 
|  | uma_handler_->SavePlayerState(render_process_id, event); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MediaInternals::AddUpdateCallback(const UpdateCallback& callback) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | update_callbacks_.push_back(callback); | 
|  |  | 
|  | base::AutoLock auto_lock(lock_); | 
|  | can_update_ = true; | 
|  | } | 
|  |  | 
|  | void MediaInternals::RemoveUpdateCallback(const UpdateCallback& callback) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | for (size_t i = 0; i < update_callbacks_.size(); ++i) { | 
|  | if (update_callbacks_[i].Equals(callback)) { | 
|  | update_callbacks_.erase(update_callbacks_.begin() + i); | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | base::AutoLock auto_lock(lock_); | 
|  | can_update_ = !update_callbacks_.empty(); | 
|  | } | 
|  |  | 
|  | bool MediaInternals::CanUpdate() { | 
|  | base::AutoLock auto_lock(lock_); | 
|  | return can_update_; | 
|  | } | 
|  |  | 
|  | void MediaInternals::SendHistoricalMediaEvents() { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | for (const auto& saved_events : saved_events_by_process_) { | 
|  | for (const auto& event : saved_events.second) { | 
|  | base::string16 update; | 
|  | if (ConvertEventToUpdate(saved_events.first, event, &update)) | 
|  | SendUpdate(update); | 
|  | } | 
|  | } | 
|  | // Do not clear the map/list here so that refreshing the UI or opening a | 
|  | // second UI still works nicely! | 
|  | } | 
|  |  | 
|  | void MediaInternals::SendAudioStreamData() { | 
|  | base::string16 audio_stream_update; | 
|  | { | 
|  | base::AutoLock auto_lock(lock_); | 
|  | audio_stream_update = SerializeUpdate("media.onReceiveAudioStreamData", | 
|  | &audio_streams_cached_data_); | 
|  | } | 
|  | SendUpdate(audio_stream_update); | 
|  | } | 
|  |  | 
|  | void MediaInternals::SendVideoCaptureDeviceCapabilities() { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::IO); | 
|  |  | 
|  | if (!CanUpdate()) | 
|  | return; | 
|  |  | 
|  | SendUpdate(SerializeUpdate("media.onReceiveVideoCaptureCapabilities", | 
|  | &video_capture_capabilities_cached_data_)); | 
|  | } | 
|  |  | 
|  | void MediaInternals::UpdateVideoCaptureDeviceCapabilities( | 
|  | const std::vector<std::tuple<media::VideoCaptureDeviceDescriptor, | 
|  | media::VideoCaptureFormats>>& | 
|  | descriptors_and_formats) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::IO); | 
|  | video_capture_capabilities_cached_data_.Clear(); | 
|  |  | 
|  | for (const auto& device_format_pair : descriptors_and_formats) { | 
|  | auto format_list = std::make_unique<base::ListValue>(); | 
|  | // TODO(nisse): Representing format information as a string, to be | 
|  | // parsed by the javascript handler, is brittle. Consider passing | 
|  | // a list of mappings instead. | 
|  |  | 
|  | const media::VideoCaptureDeviceDescriptor& descriptor = | 
|  | std::get<0>(device_format_pair); | 
|  | const media::VideoCaptureFormats& supported_formats = | 
|  | std::get<1>(device_format_pair); | 
|  | for (const auto& format : supported_formats) | 
|  | format_list->AppendString(media::VideoCaptureFormat::ToString(format)); | 
|  |  | 
|  | std::unique_ptr<base::DictionaryValue> device_dict( | 
|  | new base::DictionaryValue()); | 
|  | device_dict->SetString("id", descriptor.device_id); | 
|  | device_dict->SetString("name", descriptor.GetNameAndModel()); | 
|  | device_dict->Set("formats", std::move(format_list)); | 
|  | #if defined(OS_WIN) || defined(OS_MACOSX) || defined(OS_LINUX) || \ | 
|  | defined(OS_ANDROID) | 
|  | device_dict->SetString("captureApi", descriptor.GetCaptureApiTypeString()); | 
|  | #endif | 
|  | video_capture_capabilities_cached_data_.Append(std::move(device_dict)); | 
|  | } | 
|  |  | 
|  | SendVideoCaptureDeviceCapabilities(); | 
|  | } | 
|  |  | 
|  | std::unique_ptr<media::AudioLog> MediaInternals::CreateAudioLog( | 
|  | AudioComponent component, | 
|  | int component_id) { | 
|  | return std::make_unique<MojoAudioLogAdapter>( | 
|  | CreateMojoAudioLog(component, component_id)); | 
|  | } | 
|  |  | 
|  | media::mojom::AudioLogPtr MediaInternals::CreateMojoAudioLog( | 
|  | media::AudioLogFactory::AudioComponent component, | 
|  | int component_id, | 
|  | int render_process_id, | 
|  | int render_frame_id) { | 
|  | base::AutoLock auto_lock(lock_); | 
|  | media::mojom::AudioLogPtr audio_log_ptr; | 
|  | mojo::MakeStrongBinding(std::make_unique<AudioLogImpl>( | 
|  | owner_ids_[component]++, component, this, | 
|  | component_id, render_process_id, render_frame_id), | 
|  | mojo::MakeRequest(&audio_log_ptr)); | 
|  | return audio_log_ptr; | 
|  | } | 
|  |  | 
|  | void MediaInternals::OnProcessTerminatedForTesting(int process_id) { | 
|  | uma_handler_->OnProcessTerminated(process_id); | 
|  | } | 
|  |  | 
|  | void MediaInternals::SendUpdate(const base::string16& update) { | 
|  | // SendUpdate() may be called from any thread, but must run on the UI thread. | 
|  | if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) { | 
|  | BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, | 
|  | base::BindOnce(&MediaInternals::SendUpdate, | 
|  | base::Unretained(this), update)); | 
|  | return; | 
|  | } | 
|  |  | 
|  | for (size_t i = 0; i < update_callbacks_.size(); i++) | 
|  | update_callbacks_[i].Run(update); | 
|  | } | 
|  |  | 
|  | void MediaInternals::SaveEvent(int process_id, | 
|  | const media::MediaLogEvent& event) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  |  | 
|  | // Save the event and limit the total number per renderer. At the time of | 
|  | // writing, 512 events of the kind: { "property": value } together consume | 
|  | // ~88kb of memory on linux. | 
|  | #if defined(OS_ANDROID) | 
|  | const size_t kEventLimit = 128; | 
|  | #else | 
|  | const size_t kEventLimit = 512; | 
|  | #endif | 
|  |  | 
|  | auto& saved_events = saved_events_by_process_[process_id]; | 
|  | saved_events.push_back(event); | 
|  | if (saved_events.size() > kEventLimit) { | 
|  | // Remove all events for a given player as soon as we have to remove a | 
|  | // single event for that player to avoid showing incomplete players. | 
|  | const int id_to_remove = saved_events.front().id; | 
|  | saved_events.erase(std::remove_if(saved_events.begin(), saved_events.end(), | 
|  | [&](const media::MediaLogEvent& event) { | 
|  | return event.id == id_to_remove; | 
|  | }), | 
|  | saved_events.end()); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MediaInternals::UpdateAudioLog(AudioLogUpdateType type, | 
|  | const std::string& cache_key, | 
|  | const std::string& function, | 
|  | const base::DictionaryValue* value) { | 
|  | { | 
|  | base::AutoLock auto_lock(lock_); | 
|  | const bool has_entry = audio_streams_cached_data_.HasKey(cache_key); | 
|  | if ((type == UPDATE_IF_EXISTS || type == UPDATE_AND_DELETE) && !has_entry) { | 
|  | return; | 
|  | } else if (!has_entry) { | 
|  | DCHECK_EQ(type, CREATE); | 
|  | audio_streams_cached_data_.Set( | 
|  | cache_key, std::make_unique<base::Value>(value->Clone())); | 
|  | } else if (type == UPDATE_AND_DELETE) { | 
|  | std::unique_ptr<base::Value> out_value; | 
|  | CHECK(audio_streams_cached_data_.Remove(cache_key, &out_value)); | 
|  | } else { | 
|  | base::DictionaryValue* existing_dict = nullptr; | 
|  | CHECK( | 
|  | audio_streams_cached_data_.GetDictionary(cache_key, &existing_dict)); | 
|  | existing_dict->MergeDictionary(value); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (CanUpdate()) | 
|  | SendUpdate(SerializeUpdate(function, value)); | 
|  | } | 
|  |  | 
|  | }  // namespace content |