Home | History | Annotate | Download | only in media
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "chrome/browser/media/media_stream_capture_indicator.h"
      6 
      7 #include "base/bind.h"
      8 #include "base/i18n/rtl.h"
      9 #include "base/logging.h"
     10 #include "base/memory/scoped_ptr.h"
     11 #include "base/prefs/pref_service.h"
     12 #include "base/strings/utf_string_conversions.h"
     13 #include "chrome/app/chrome_command_ids.h"
     14 #include "chrome/browser/browser_process.h"
     15 #include "chrome/browser/extensions/extension_service.h"
     16 #include "chrome/browser/profiles/profile.h"
     17 #include "chrome/browser/status_icons/status_icon.h"
     18 #include "chrome/browser/status_icons/status_tray.h"
     19 #include "chrome/browser/tab_contents/tab_util.h"
     20 #include "chrome/common/pref_names.h"
     21 #include "content/public/browser/browser_thread.h"
     22 #include "content/public/browser/content_browser_client.h"
     23 #include "content/public/browser/invalidate_type.h"
     24 #include "content/public/browser/web_contents.h"
     25 #include "content/public/browser/web_contents_delegate.h"
     26 #include "content/public/browser/web_contents_observer.h"
     27 #include "extensions/common/extension.h"
     28 #include "grit/chromium_strings.h"
     29 #include "grit/generated_resources.h"
     30 #include "grit/theme_resources.h"
     31 #include "net/base/net_util.h"
     32 #include "ui/base/l10n/l10n_util.h"
     33 #include "ui/base/resource/resource_bundle.h"
     34 #include "ui/gfx/image/image_skia.h"
     35 
     36 using content::BrowserThread;
     37 using content::WebContents;
     38 
     39 namespace {
     40 
     41 const extensions::Extension* GetExtension(WebContents* web_contents) {
     42   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
     43 
     44   if (!web_contents)
     45     return NULL;
     46 
     47   Profile* profile =
     48       Profile::FromBrowserContext(web_contents->GetBrowserContext());
     49   if (!profile)
     50     return NULL;
     51 
     52   ExtensionService* extension_service = profile->GetExtensionService();
     53   if (!extension_service)
     54     return NULL;
     55 
     56   return extension_service->extensions()->GetExtensionOrAppByURL(
     57       web_contents->GetURL());
     58 }
     59 
     60 // Gets the security originator of the tab. It returns a string with no '/'
     61 // at the end to display in the UI.
     62 base::string16 GetSecurityOrigin(WebContents* web_contents) {
     63   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
     64 
     65   if (!web_contents)
     66     return base::string16();
     67 
     68   std::string security_origin = web_contents->GetURL().GetOrigin().spec();
     69 
     70   // Remove the last character if it is a '/'.
     71   if (!security_origin.empty()) {
     72     std::string::iterator it = security_origin.end() - 1;
     73     if (*it == '/')
     74       security_origin.erase(it);
     75   }
     76 
     77   return UTF8ToUTF16(security_origin);
     78 }
     79 
     80 base::string16 GetTitle(WebContents* web_contents) {
     81   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
     82 
     83   if (!web_contents)
     84     return base::string16();
     85 
     86   const extensions::Extension* const extension = GetExtension(web_contents);
     87   if (extension)
     88     return UTF8ToUTF16(extension->name());
     89 
     90   base::string16 tab_title = web_contents->GetTitle();
     91 
     92   if (tab_title.empty()) {
     93     // If the page's title is empty use its security originator.
     94     tab_title = GetSecurityOrigin(web_contents);
     95   } else {
     96     // If the page's title matches its URL, use its security originator.
     97     Profile* profile =
     98         Profile::FromBrowserContext(web_contents->GetBrowserContext());
     99     std::string languages =
    100         profile->GetPrefs()->GetString(prefs::kAcceptLanguages);
    101     if (tab_title == net::FormatUrl(web_contents->GetURL(), languages))
    102       tab_title = GetSecurityOrigin(web_contents);
    103   }
    104 
    105   return tab_title;
    106 }
    107 
    108 }  // namespace
    109 
    110 // Stores usage counts for all the capture devices associated with a single
    111 // WebContents instance. Instances of this class are owned by
    112 // MediaStreamCaptureIndicator. They also observe for the destruction of the
    113 // WebContents instances and delete themselves when corresponding WebContents is
    114 // deleted.
    115 class MediaStreamCaptureIndicator::WebContentsDeviceUsage
    116     : public content::WebContentsObserver {
    117  public:
    118   explicit WebContentsDeviceUsage(
    119       scoped_refptr<MediaStreamCaptureIndicator> indicator,
    120       WebContents* web_contents)
    121       : WebContentsObserver(web_contents),
    122         indicator_(indicator),
    123         audio_ref_count_(0),
    124         video_ref_count_(0),
    125         mirroring_ref_count_(0),
    126         weak_factory_(this) {
    127   }
    128 
    129   bool IsCapturingAudio() const { return audio_ref_count_ > 0; }
    130   bool IsCapturingVideo() const { return video_ref_count_ > 0; }
    131   bool IsMirroring() const { return mirroring_ref_count_ > 0; }
    132 
    133   scoped_ptr<content::MediaStreamUI> RegisterMediaStream(
    134       const content::MediaStreamDevices& devices);
    135 
    136   // Increment ref-counts up based on the type of each device provided.
    137   void AddDevices(const content::MediaStreamDevices& devices);
    138 
    139   // Decrement ref-counts up based on the type of each device provided.
    140   void RemoveDevices(const content::MediaStreamDevices& devices);
    141 
    142  private:
    143   // content::WebContentsObserver overrides.
    144   virtual void WebContentsDestroyed(WebContents* web_contents) OVERRIDE {
    145     indicator_->UnregisterWebContents(web_contents);
    146     delete this;
    147   }
    148 
    149   scoped_refptr<MediaStreamCaptureIndicator> indicator_;
    150   int audio_ref_count_;
    151   int video_ref_count_;
    152   int mirroring_ref_count_;
    153 
    154   base::WeakPtrFactory<WebContentsDeviceUsage> weak_factory_;
    155 
    156   DISALLOW_COPY_AND_ASSIGN(WebContentsDeviceUsage);
    157 };
    158 
    159 // Implements MediaStreamUI interface. Instances of this class are created for
    160 // each MediaStream and their ownership is passed to MediaStream implementation
    161 // in the content layer. Each UIDelegate keeps a weak pointer to the
    162 // corresponding WebContentsDeviceUsage object to deliver updates about state of
    163 // the stream.
    164 class MediaStreamCaptureIndicator::UIDelegate
    165     : public content::MediaStreamUI {
    166  public:
    167   UIDelegate(base::WeakPtr<WebContentsDeviceUsage> device_usage,
    168              const content::MediaStreamDevices& devices)
    169       : device_usage_(device_usage),
    170         devices_(devices),
    171         started_(false) {
    172     DCHECK(!devices_.empty());
    173   }
    174 
    175   virtual ~UIDelegate() {
    176     if (started_ && device_usage_.get())
    177       device_usage_->RemoveDevices(devices_);
    178   }
    179 
    180  private:
    181   // content::MediaStreamUI interface.
    182   virtual void OnStarted(const base::Closure& close_callback) OVERRIDE {
    183     DCHECK(!started_);
    184     started_ = true;
    185     if (device_usage_.get())
    186       device_usage_->AddDevices(devices_);
    187   }
    188 
    189   base::WeakPtr<WebContentsDeviceUsage> device_usage_;
    190   content::MediaStreamDevices devices_;
    191   bool started_;
    192 
    193   DISALLOW_COPY_AND_ASSIGN(UIDelegate);
    194 };
    195 
    196 
    197 scoped_ptr<content::MediaStreamUI>
    198 MediaStreamCaptureIndicator::WebContentsDeviceUsage::RegisterMediaStream(
    199     const content::MediaStreamDevices& devices) {
    200   return scoped_ptr<content::MediaStreamUI>(new UIDelegate(
    201       weak_factory_.GetWeakPtr(), devices));
    202 }
    203 
    204 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::AddDevices(
    205     const content::MediaStreamDevices& devices) {
    206   for (content::MediaStreamDevices::const_iterator it = devices.begin();
    207        it != devices.end(); ++it) {
    208     if (it->type == content::MEDIA_TAB_AUDIO_CAPTURE ||
    209         it->type == content::MEDIA_TAB_VIDEO_CAPTURE) {
    210       ++mirroring_ref_count_;
    211     } else if (content::IsAudioMediaType(it->type)) {
    212       ++audio_ref_count_;
    213     } else if (content::IsVideoMediaType(it->type)) {
    214       ++video_ref_count_;
    215     } else {
    216       NOTIMPLEMENTED();
    217     }
    218   }
    219 
    220   if (web_contents())
    221     web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
    222 
    223   indicator_->UpdateNotificationUserInterface();
    224 }
    225 
    226 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::RemoveDevices(
    227     const content::MediaStreamDevices& devices) {
    228   for (content::MediaStreamDevices::const_iterator it = devices.begin();
    229        it != devices.end(); ++it) {
    230     if (it->type == content::MEDIA_TAB_AUDIO_CAPTURE ||
    231         it->type == content::MEDIA_TAB_VIDEO_CAPTURE) {
    232       --mirroring_ref_count_;
    233     } else if (content::IsAudioMediaType(it->type)) {
    234       --audio_ref_count_;
    235     } else if (content::IsVideoMediaType(it->type)) {
    236       --video_ref_count_;
    237     } else {
    238       NOTIMPLEMENTED();
    239     }
    240   }
    241 
    242   DCHECK_GE(audio_ref_count_, 0);
    243   DCHECK_GE(video_ref_count_, 0);
    244   DCHECK_GE(mirroring_ref_count_, 0);
    245 
    246   web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
    247   indicator_->UpdateNotificationUserInterface();
    248 }
    249 
    250 MediaStreamCaptureIndicator::MediaStreamCaptureIndicator()
    251     : status_icon_(NULL),
    252       mic_image_(NULL),
    253       camera_image_(NULL) {
    254 }
    255 
    256 MediaStreamCaptureIndicator::~MediaStreamCaptureIndicator() {
    257   // The user is responsible for cleaning up by reporting the closure of any
    258   // opened devices.  However, there exists a race condition at shutdown: The UI
    259   // thread may be stopped before CaptureDevicesClosed() posts the task to
    260   // invoke DoDevicesClosedOnUIThread().  In this case, usage_map_ won't be
    261   // empty like it should.
    262   DCHECK(usage_map_.empty() ||
    263          !BrowserThread::IsMessageLoopValid(BrowserThread::UI));
    264 
    265   // Free any WebContentsDeviceUsage objects left over.
    266   for (UsageMap::const_iterator it = usage_map_.begin(); it != usage_map_.end();
    267        ++it) {
    268     delete it->second;
    269   }
    270 }
    271 
    272 scoped_ptr<content::MediaStreamUI>
    273 MediaStreamCaptureIndicator::RegisterMediaStream(
    274     content::WebContents* web_contents,
    275     const content::MediaStreamDevices& devices) {
    276   WebContentsDeviceUsage*& usage = usage_map_[web_contents];
    277   if (!usage)
    278     usage = new WebContentsDeviceUsage(this, web_contents);
    279   return usage->RegisterMediaStream(devices);
    280 }
    281 
    282 void MediaStreamCaptureIndicator::ExecuteCommand(int command_id,
    283                                                  int event_flags) {
    284   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    285 
    286   const int index =
    287       command_id - IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST;
    288   DCHECK_LE(0, index);
    289   DCHECK_GT(static_cast<int>(command_targets_.size()), index);
    290   WebContents* const web_contents = command_targets_[index];
    291   UsageMap::const_iterator it = usage_map_.find(web_contents);
    292   if (it == usage_map_.end())
    293     return;
    294   web_contents->GetDelegate()->ActivateContents(web_contents);
    295 }
    296 
    297 bool MediaStreamCaptureIndicator::IsCapturingUserMedia(
    298     content::WebContents* web_contents) const {
    299   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    300 
    301   UsageMap::const_iterator it = usage_map_.find(web_contents);
    302   return (it != usage_map_.end() &&
    303           (it->second->IsCapturingAudio() || it->second->IsCapturingVideo()));
    304 }
    305 
    306 bool MediaStreamCaptureIndicator::IsCapturingVideo(
    307     content::WebContents* web_contents) const {
    308   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    309 
    310   UsageMap::const_iterator it = usage_map_.find(web_contents);
    311   return (it != usage_map_.end() && it->second->IsCapturingVideo());
    312 }
    313 
    314 bool MediaStreamCaptureIndicator::IsCapturingAudio(
    315     content::WebContents* web_contents) const {
    316   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    317 
    318   UsageMap::const_iterator it = usage_map_.find(web_contents);
    319   return (it != usage_map_.end() && it->second->IsCapturingAudio());
    320 }
    321 
    322 bool MediaStreamCaptureIndicator::IsBeingMirrored(
    323     content::WebContents* web_contents) const {
    324   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    325 
    326   UsageMap::const_iterator it = usage_map_.find(web_contents);
    327   return it != usage_map_.end() && it->second->IsMirroring();
    328 }
    329 
    330 void MediaStreamCaptureIndicator::UnregisterWebContents(
    331     WebContents* web_contents) {
    332   usage_map_.erase(web_contents);
    333   UpdateNotificationUserInterface();
    334 }
    335 
    336 void MediaStreamCaptureIndicator::MaybeCreateStatusTrayIcon(bool audio,
    337                                                             bool video) {
    338   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    339   if (status_icon_)
    340     return;
    341 
    342   // If there is no browser process, we should not create the status tray.
    343   if (!g_browser_process)
    344     return;
    345 
    346   StatusTray* status_tray = g_browser_process->status_tray();
    347   if (!status_tray)
    348     return;
    349 
    350   EnsureStatusTrayIconResources();
    351 
    352   gfx::ImageSkia image;
    353   base::string16 tool_tip;
    354   GetStatusTrayIconInfo(audio, video, &image, &tool_tip);
    355   DCHECK(!image.isNull());
    356   DCHECK(!tool_tip.empty());
    357 
    358   status_icon_ = status_tray->CreateStatusIcon(
    359       StatusTray::MEDIA_STREAM_CAPTURE_ICON, image, tool_tip);
    360 }
    361 
    362 void MediaStreamCaptureIndicator::EnsureStatusTrayIconResources() {
    363   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    364   if (!mic_image_) {
    365     mic_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
    366         IDR_INFOBAR_MEDIA_STREAM_MIC);
    367   }
    368   if (!camera_image_) {
    369     camera_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
    370         IDR_INFOBAR_MEDIA_STREAM_CAMERA);
    371   }
    372   DCHECK(mic_image_);
    373   DCHECK(camera_image_);
    374 }
    375 
    376 void MediaStreamCaptureIndicator::MaybeDestroyStatusTrayIcon() {
    377   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    378 
    379   if (!status_icon_)
    380     return;
    381 
    382   // If there is no browser process, we should not do anything.
    383   if (!g_browser_process)
    384     return;
    385 
    386   StatusTray* status_tray = g_browser_process->status_tray();
    387   if (status_tray != NULL) {
    388     status_tray->RemoveStatusIcon(status_icon_);
    389     status_icon_ = NULL;
    390   }
    391 }
    392 
    393 void MediaStreamCaptureIndicator::UpdateNotificationUserInterface() {
    394   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    395   scoped_ptr<StatusIconMenuModel> menu(new StatusIconMenuModel(this));
    396 
    397   bool audio = false;
    398   bool video = false;
    399   int command_id = IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST;
    400   command_targets_.clear();
    401 
    402   for (UsageMap::const_iterator iter = usage_map_.begin();
    403        iter != usage_map_.end(); ++iter) {
    404     // Check if any audio and video devices have been used.
    405     const WebContentsDeviceUsage& usage = *iter->second;
    406     WebContents* const web_contents = iter->first;
    407 
    408     // Audio/video icon is shown only for extensions or on Android.
    409     // For regular tabs on desktop, we show an indicator in the tab icon.
    410     if ((usage.IsCapturingAudio() || usage.IsCapturingVideo())
    411 #if !defined(OS_ANDROID)
    412         && GetExtension(web_contents)
    413 #endif
    414         ) {
    415       audio = audio || usage.IsCapturingAudio();
    416       video = video || usage.IsCapturingVideo();
    417 
    418       command_targets_.push_back(web_contents);
    419       menu->AddItem(command_id, GetTitle(web_contents));
    420 
    421       // If the menu item is not a label, enable it.
    422       menu->SetCommandIdEnabled(command_id,
    423                                 command_id != IDC_MinimumLabelValue);
    424 
    425       // If reaching the maximum number, no more item will be added to the menu.
    426       if (command_id == IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_LAST)
    427         break;
    428       ++command_id;
    429     }
    430   }
    431 
    432   if (command_targets_.empty()) {
    433     MaybeDestroyStatusTrayIcon();
    434     return;
    435   }
    436 
    437   // The icon will take the ownership of the passed context menu.
    438   MaybeCreateStatusTrayIcon(audio, video);
    439   if (status_icon_) {
    440     status_icon_->SetContextMenu(menu.Pass());
    441   }
    442 }
    443 
    444 void MediaStreamCaptureIndicator::GetStatusTrayIconInfo(
    445     bool audio,
    446     bool video,
    447     gfx::ImageSkia* image,
    448     base::string16* tool_tip) {
    449   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    450   DCHECK(audio || video);
    451 
    452   int message_id = 0;
    453   if (audio && video) {
    454     message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_AND_VIDEO;
    455     *image = *camera_image_;
    456   } else if (audio && !video) {
    457     message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_ONLY;
    458     *image = *mic_image_;
    459   } else if (!audio && video) {
    460     message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_VIDEO_ONLY;
    461     *image = *camera_image_;
    462   }
    463 
    464   *tool_tip = l10n_util::GetStringUTF16(message_id);
    465 }
    466