1// Copyright 2013 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/native_desktop_media_list.h"
6
7#include <map>
8#include <set>
9#include <sstream>
10
11#include "base/hash.h"
12#include "base/logging.h"
13#include "base/strings/utf_string_conversions.h"
14#include "base/threading/sequenced_worker_pool.h"
15#include "chrome/browser/media/desktop_media_list_observer.h"
16#include "chrome/grit/generated_resources.h"
17#include "content/public/browser/browser_thread.h"
18#include "media/base/video_util.h"
19#include "third_party/libyuv/include/libyuv/scale_argb.h"
20#include "third_party/skia/include/core/SkBitmap.h"
21#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h"
22#include "third_party/webrtc/modules/desktop_capture/screen_capturer.h"
23#include "third_party/webrtc/modules/desktop_capture/window_capturer.h"
24#include "ui/base/l10n/l10n_util.h"
25#include "ui/gfx/skia_util.h"
26
27using content::BrowserThread;
28using content::DesktopMediaID;
29
30namespace {
31
32// Update the list every second.
33const int kDefaultUpdatePeriod = 1000;
34
35// Returns a hash of a DesktopFrame content to detect when image for a desktop
36// media source has changed.
37uint32 GetFrameHash(webrtc::DesktopFrame* frame) {
38  int data_size = frame->stride() * frame->size().height();
39  return base::SuperFastHash(reinterpret_cast<char*>(frame->data()), data_size);
40}
41
42gfx::ImageSkia ScaleDesktopFrame(scoped_ptr<webrtc::DesktopFrame> frame,
43                                 gfx::Size size) {
44  gfx::Rect scaled_rect = media::ComputeLetterboxRegion(
45      gfx::Rect(0, 0, size.width(), size.height()),
46      gfx::Size(frame->size().width(), frame->size().height()));
47
48  SkBitmap result;
49  result.allocN32Pixels(scaled_rect.width(), scaled_rect.height(), true);
50  result.lockPixels();
51
52  uint8* pixels_data = reinterpret_cast<uint8*>(result.getPixels());
53  libyuv::ARGBScale(frame->data(), frame->stride(),
54                    frame->size().width(), frame->size().height(),
55                    pixels_data, result.rowBytes(),
56                    scaled_rect.width(), scaled_rect.height(),
57                    libyuv::kFilterBilinear);
58
59  // Set alpha channel values to 255 for all pixels.
60  // TODO(sergeyu): Fix screen/window capturers to capture alpha channel and
61  // remove this code. Currently screen/window capturers (at least some
62  // implementations) only capture R, G and B channels and set Alpha to 0.
63  // crbug.com/264424
64  for (int y = 0; y < result.height(); ++y) {
65    for (int x = 0; x < result.width(); ++x) {
66      pixels_data[result.rowBytes() * y + x * result.bytesPerPixel() + 3] =
67          0xff;
68    }
69  }
70
71  result.unlockPixels();
72
73  return gfx::ImageSkia::CreateFrom1xBitmap(result);
74}
75
76}  // namespace
77
78NativeDesktopMediaList::SourceDescription::SourceDescription(
79    DesktopMediaID id,
80    const base::string16& name)
81    : id(id),
82      name(name) {
83}
84
85class NativeDesktopMediaList::Worker
86    : public webrtc::DesktopCapturer::Callback {
87 public:
88  Worker(base::WeakPtr<NativeDesktopMediaList> media_list,
89         scoped_ptr<webrtc::ScreenCapturer> screen_capturer,
90         scoped_ptr<webrtc::WindowCapturer> window_capturer);
91  virtual ~Worker();
92
93  void Refresh(const gfx::Size& thumbnail_size,
94               content::DesktopMediaID::Id view_dialog_id);
95
96 private:
97  typedef std::map<DesktopMediaID, uint32> ImageHashesMap;
98
99  // webrtc::DesktopCapturer::Callback interface.
100  virtual webrtc::SharedMemory* CreateSharedMemory(size_t size) OVERRIDE;
101  virtual void OnCaptureCompleted(webrtc::DesktopFrame* frame) OVERRIDE;
102
103  base::WeakPtr<NativeDesktopMediaList> media_list_;
104
105  scoped_ptr<webrtc::ScreenCapturer> screen_capturer_;
106  scoped_ptr<webrtc::WindowCapturer> window_capturer_;
107
108  scoped_ptr<webrtc::DesktopFrame> current_frame_;
109
110  ImageHashesMap image_hashes_;
111
112  DISALLOW_COPY_AND_ASSIGN(Worker);
113};
114
115NativeDesktopMediaList::Worker::Worker(
116    base::WeakPtr<NativeDesktopMediaList> media_list,
117    scoped_ptr<webrtc::ScreenCapturer> screen_capturer,
118    scoped_ptr<webrtc::WindowCapturer> window_capturer)
119    : media_list_(media_list),
120      screen_capturer_(screen_capturer.Pass()),
121      window_capturer_(window_capturer.Pass()) {
122  if (screen_capturer_)
123    screen_capturer_->Start(this);
124  if (window_capturer_)
125    window_capturer_->Start(this);
126}
127
128NativeDesktopMediaList::Worker::~Worker() {}
129
130void NativeDesktopMediaList::Worker::Refresh(
131    const gfx::Size& thumbnail_size,
132    content::DesktopMediaID::Id view_dialog_id) {
133  std::vector<SourceDescription> sources;
134
135  if (screen_capturer_) {
136    webrtc::ScreenCapturer::ScreenList screens;
137    if (screen_capturer_->GetScreenList(&screens)) {
138      bool mutiple_screens = screens.size() > 1;
139      base::string16 title;
140      for (size_t i = 0; i < screens.size(); ++i) {
141        if (mutiple_screens) {
142          title = l10n_util::GetStringFUTF16Int(
143              IDS_DESKTOP_MEDIA_PICKER_MULTIPLE_SCREEN_NAME,
144              static_cast<int>(i + 1));
145        } else {
146          title = l10n_util::GetStringUTF16(
147              IDS_DESKTOP_MEDIA_PICKER_SINGLE_SCREEN_NAME);
148        }
149        sources.push_back(SourceDescription(DesktopMediaID(
150            DesktopMediaID::TYPE_SCREEN, screens[i].id), title));
151      }
152    }
153  }
154
155  if (window_capturer_) {
156    webrtc::WindowCapturer::WindowList windows;
157    if (window_capturer_->GetWindowList(&windows)) {
158      for (webrtc::WindowCapturer::WindowList::iterator it = windows.begin();
159           it != windows.end(); ++it) {
160        // Skip the picker dialog window.
161        if (it->id != view_dialog_id) {
162          sources.push_back(SourceDescription(
163              DesktopMediaID(DesktopMediaID::TYPE_WINDOW, it->id),
164              base::UTF8ToUTF16(it->title)));
165        }
166      }
167    }
168  }
169  // Update list of windows before updating thumbnails.
170  BrowserThread::PostTask(
171      BrowserThread::UI, FROM_HERE,
172      base::Bind(&NativeDesktopMediaList::OnSourcesList,
173                 media_list_, sources));
174
175  ImageHashesMap new_image_hashes;
176
177  // Get a thumbnail for each source.
178  for (size_t i = 0; i < sources.size(); ++i) {
179    SourceDescription& source = sources[i];
180    switch (source.id.type) {
181      case DesktopMediaID::TYPE_SCREEN:
182        if (!screen_capturer_->SelectScreen(source.id.id))
183          continue;
184        screen_capturer_->Capture(webrtc::DesktopRegion());
185        break;
186
187      case DesktopMediaID::TYPE_WINDOW:
188        if (!window_capturer_->SelectWindow(source.id.id))
189          continue;
190        window_capturer_->Capture(webrtc::DesktopRegion());
191        break;
192
193      default:
194        NOTREACHED();
195    }
196
197    // Expect that DesktopCapturer to always captures frames synchronously.
198    // |current_frame_| may be NULL if capture failed (e.g. because window has
199    // been closed).
200    if (current_frame_) {
201      uint32 frame_hash = GetFrameHash(current_frame_.get());
202      new_image_hashes[source.id] = frame_hash;
203
204      // Scale the image only if it has changed.
205      ImageHashesMap::iterator it = image_hashes_.find(source.id);
206      if (it == image_hashes_.end() || it->second != frame_hash) {
207        gfx::ImageSkia thumbnail =
208            ScaleDesktopFrame(current_frame_.Pass(), thumbnail_size);
209        BrowserThread::PostTask(
210            BrowserThread::UI, FROM_HERE,
211            base::Bind(&NativeDesktopMediaList::OnSourceThumbnail,
212                        media_list_, i, thumbnail));
213      }
214    }
215  }
216
217  image_hashes_.swap(new_image_hashes);
218
219  BrowserThread::PostTask(
220      BrowserThread::UI, FROM_HERE,
221      base::Bind(&NativeDesktopMediaList::OnRefreshFinished, media_list_));
222}
223
224webrtc::SharedMemory* NativeDesktopMediaList::Worker::CreateSharedMemory(
225    size_t size) {
226  return NULL;
227}
228
229void NativeDesktopMediaList::Worker::OnCaptureCompleted(
230    webrtc::DesktopFrame* frame) {
231  current_frame_.reset(frame);
232}
233
234NativeDesktopMediaList::NativeDesktopMediaList(
235    scoped_ptr<webrtc::ScreenCapturer> screen_capturer,
236    scoped_ptr<webrtc::WindowCapturer> window_capturer)
237    : screen_capturer_(screen_capturer.Pass()),
238      window_capturer_(window_capturer.Pass()),
239      update_period_(base::TimeDelta::FromMilliseconds(kDefaultUpdatePeriod)),
240      thumbnail_size_(100, 100),
241      view_dialog_id_(-1),
242      observer_(NULL),
243      weak_factory_(this) {
244  base::SequencedWorkerPool* worker_pool = BrowserThread::GetBlockingPool();
245  capture_task_runner_ = worker_pool->GetSequencedTaskRunner(
246      worker_pool->GetSequenceToken());
247}
248
249NativeDesktopMediaList::~NativeDesktopMediaList() {
250  capture_task_runner_->DeleteSoon(FROM_HERE, worker_.release());
251}
252
253void NativeDesktopMediaList::SetUpdatePeriod(base::TimeDelta period) {
254  DCHECK(!observer_);
255  update_period_ = period;
256}
257
258void NativeDesktopMediaList::SetThumbnailSize(
259    const gfx::Size& thumbnail_size) {
260  thumbnail_size_ = thumbnail_size;
261}
262
263void NativeDesktopMediaList::SetViewDialogWindowId(
264    content::DesktopMediaID::Id dialog_id) {
265  view_dialog_id_ = dialog_id;
266}
267
268void NativeDesktopMediaList::StartUpdating(DesktopMediaListObserver* observer) {
269  DCHECK(!observer_);
270  DCHECK(screen_capturer_ || window_capturer_);
271
272  observer_ = observer;
273
274  worker_.reset(new Worker(weak_factory_.GetWeakPtr(),
275                           screen_capturer_.Pass(), window_capturer_.Pass()));
276  Refresh();
277}
278
279int NativeDesktopMediaList::GetSourceCount() const {
280  return sources_.size();
281}
282
283const DesktopMediaList::Source& NativeDesktopMediaList::GetSource(
284    int index) const {
285  return sources_[index];
286}
287
288void NativeDesktopMediaList::Refresh() {
289  capture_task_runner_->PostTask(
290      FROM_HERE, base::Bind(&Worker::Refresh, base::Unretained(worker_.get()),
291                            thumbnail_size_, view_dialog_id_));
292}
293
294void NativeDesktopMediaList::OnSourcesList(
295    const std::vector<SourceDescription>& new_sources) {
296  typedef std::set<content::DesktopMediaID> SourceSet;
297  SourceSet new_source_set;
298  for (size_t i = 0; i < new_sources.size(); ++i) {
299    new_source_set.insert(new_sources[i].id);
300  }
301  // Iterate through the old sources to find the removed sources.
302  for (size_t i = 0; i < sources_.size(); ++i) {
303    if (new_source_set.find(sources_[i].id) == new_source_set.end()) {
304      sources_.erase(sources_.begin() + i);
305      observer_->OnSourceRemoved(i);
306      --i;
307    }
308  }
309  // Iterate through the new sources to find the added sources.
310  if (new_sources.size() > sources_.size()) {
311    SourceSet old_source_set;
312    for (size_t i = 0; i < sources_.size(); ++i) {
313      old_source_set.insert(sources_[i].id);
314    }
315
316    for (size_t i = 0; i < new_sources.size(); ++i) {
317      if (old_source_set.find(new_sources[i].id) == old_source_set.end()) {
318        sources_.insert(sources_.begin() + i, Source());
319        sources_[i].id = new_sources[i].id;
320        sources_[i].name = new_sources[i].name;
321        observer_->OnSourceAdded(i);
322      }
323    }
324  }
325  DCHECK_EQ(new_sources.size(), sources_.size());
326
327  // Find the moved/changed sources.
328  size_t pos = 0;
329  while (pos < sources_.size()) {
330    if (!(sources_[pos].id == new_sources[pos].id)) {
331      // Find the source that should be moved to |pos|, starting from |pos + 1|
332      // of |sources_|, because entries before |pos| should have been sorted.
333      size_t old_pos = pos + 1;
334      for (; old_pos < sources_.size(); ++old_pos) {
335        if (sources_[old_pos].id == new_sources[pos].id)
336          break;
337      }
338      DCHECK(sources_[old_pos].id == new_sources[pos].id);
339
340      // Move the source from |old_pos| to |pos|.
341      Source temp = sources_[old_pos];
342      sources_.erase(sources_.begin() + old_pos);
343      sources_.insert(sources_.begin() + pos, temp);
344
345      observer_->OnSourceMoved(old_pos, pos);
346    }
347
348    if (sources_[pos].name != new_sources[pos].name) {
349      sources_[pos].name = new_sources[pos].name;
350      observer_->OnSourceNameChanged(pos);
351    }
352    ++pos;
353  }
354}
355
356void NativeDesktopMediaList::OnSourceThumbnail(
357    int index,
358    const gfx::ImageSkia& image) {
359  DCHECK_LT(index, static_cast<int>(sources_.size()));
360  sources_[index].thumbnail = image;
361  observer_->OnSourceThumbnailChanged(index);
362}
363
364void NativeDesktopMediaList::OnRefreshFinished() {
365  BrowserThread::PostDelayedTask(
366      BrowserThread::UI, FROM_HERE,
367      base::Bind(&NativeDesktopMediaList::Refresh,
368                 weak_factory_.GetWeakPtr()),
369      update_period_);
370}
371