OLD | NEW |
---|---|
1 // Copyright 2013 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "chrome/browser/media/native_desktop_media_list.h" | 5 #include "chrome/browser/media/native_desktop_media_list.h" |
6 | 6 |
7 #include "base/hash.h" | 7 #include "base/hash.h" |
8 #include "base/strings/utf_string_conversions.h" | 8 #include "base/strings/utf_string_conversions.h" |
9 #include "base/threading/sequenced_worker_pool.h" | 9 #include "base/threading/sequenced_worker_pool.h" |
10 #include "chrome/browser/media/desktop_media_list_observer.h" | 10 #include "chrome/browser/media/desktop_media_list_observer.h" |
11 #include "chrome/browser/ui/browser.h" | |
12 #include "chrome/browser/ui/browser_list.h" | |
13 #include "chrome/browser/ui/browser_window.h" | |
11 #include "chrome/grit/generated_resources.h" | 14 #include "chrome/grit/generated_resources.h" |
12 #include "content/public/browser/browser_thread.h" | 15 #include "content/public/browser/browser_thread.h" |
13 #include "media/base/video_util.h" | 16 #include "media/base/video_util.h" |
14 #include "third_party/libyuv/include/libyuv/scale_argb.h" | 17 #include "third_party/libyuv/include/libyuv/scale_argb.h" |
15 #include "third_party/skia/include/core/SkBitmap.h" | 18 #include "third_party/skia/include/core/SkBitmap.h" |
16 #include "third_party/webrtc/modules/desktop_capture/desktop_frame.h" | 19 #include "third_party/webrtc/modules/desktop_capture/desktop_frame.h" |
17 #include "third_party/webrtc/modules/desktop_capture/screen_capturer.h" | 20 #include "third_party/webrtc/modules/desktop_capture/screen_capturer.h" |
18 #include "third_party/webrtc/modules/desktop_capture/window_capturer.h" | 21 #include "third_party/webrtc/modules/desktop_capture/window_capturer.h" |
22 #include "ui/aura/window.h" | |
23 #include "ui/aura/window_tree_host.h" | |
19 #include "ui/base/l10n/l10n_util.h" | 24 #include "ui/base/l10n/l10n_util.h" |
25 #include "ui/gfx/native_widget_types.h" | |
26 #include "ui/snapshot/snapshot.h" | |
20 | 27 |
21 using content::BrowserThread; | 28 using content::BrowserThread; |
22 using content::DesktopMediaID; | 29 using content::DesktopMediaID; |
23 | 30 |
24 namespace { | 31 namespace { |
25 | 32 |
26 // Update the list every second. | 33 // Update the list every second. |
27 const int kDefaultUpdatePeriod = 1000; | 34 const int kDefaultUpdatePeriod = 1000; |
28 | 35 |
29 // Returns a hash of a DesktopFrame content to detect when image for a desktop | 36 // Returns a hash of a DesktopFrame content to detect when image for a desktop |
30 // media source has changed. | 37 // media source has changed. |
31 uint32_t GetFrameHash(webrtc::DesktopFrame* frame) { | 38 uint32_t GetFrameHash(webrtc::DesktopFrame* frame) { |
32 int data_size = frame->stride() * frame->size().height(); | 39 int data_size = frame->stride() * frame->size().height(); |
33 return base::SuperFastHash(reinterpret_cast<char*>(frame->data()), data_size); | 40 return base::Hash(reinterpret_cast<char*>(frame->data()), data_size); |
34 } | 41 } |
35 | 42 |
36 gfx::ImageSkia ScaleDesktopFrame(scoped_ptr<webrtc::DesktopFrame> frame, | 43 gfx::ImageSkia ScaleDesktopFrame(scoped_ptr<webrtc::DesktopFrame> frame, |
37 gfx::Size size) { | 44 gfx::Size size) { |
38 gfx::Rect scaled_rect = media::ComputeLetterboxRegion( | 45 gfx::Rect scaled_rect = media::ComputeLetterboxRegion( |
39 gfx::Rect(0, 0, size.width(), size.height()), | 46 gfx::Rect(0, 0, size.width(), size.height()), |
40 gfx::Size(frame->size().width(), frame->size().height())); | 47 gfx::Size(frame->size().width(), frame->size().height())); |
41 | 48 |
42 SkBitmap result; | 49 SkBitmap result; |
43 result.allocN32Pixels(scaled_rect.width(), scaled_rect.height(), true); | 50 result.allocN32Pixels(scaled_rect.width(), scaled_rect.height(), true); |
(...skipping 16 matching lines...) Expand all Loading... | |
60 pixels_data[result.rowBytes() * y + x * result.bytesPerPixel() + 3] = | 67 pixels_data[result.rowBytes() * y + x * result.bytesPerPixel() + 3] = |
61 0xff; | 68 0xff; |
62 } | 69 } |
63 } | 70 } |
64 | 71 |
65 result.unlockPixels(); | 72 result.unlockPixels(); |
66 | 73 |
67 return gfx::ImageSkia::CreateFrom1xBitmap(result); | 74 return gfx::ImageSkia::CreateFrom1xBitmap(result); |
68 } | 75 } |
69 | 76 |
77 #if defined(USE_AURA) | |
78 | |
79 NativeDesktopMediaList::NativeAuraIdMap GetBrowserNativeAuraIdMap() { | |
80 NativeDesktopMediaList::NativeAuraIdMap id_map; | |
81 for (auto* browser : *BrowserList::GetInstance()) { | |
82 aura::Window* aura_window = browser->window()->GetNativeWindow(); | |
83 if (!aura_window) | |
84 continue; | |
85 aura::WindowTreeHost* host = aura_window->GetHost(); | |
86 if (!host) | |
87 continue; | |
88 gfx::AcceleratedWidget widget = host->GetAcceleratedWidget(); | |
89 #if defined(OS_WIN) | |
90 DesktopMediaID::Id native_id = reinterpret_cast<DesktopMediaID::Id>(widget); | |
91 #else | |
92 DesktopMediaID::Id native_id = widget; | |
93 #endif | |
94 DesktopMediaID media_id = DesktopMediaID::RegisterAuraWindow( | |
95 DesktopMediaID::TYPE_WINDOW, aura_window); | |
96 id_map[native_id] = media_id.aura_id; | |
97 } | |
98 | |
99 return id_map; | |
100 } | |
101 | |
102 #endif // defined(USE_AURA) | |
103 | |
70 } // namespace | 104 } // namespace |
71 | 105 |
72 class NativeDesktopMediaList::Worker | 106 class NativeDesktopMediaList::Worker |
73 : public webrtc::DesktopCapturer::Callback { | 107 : public webrtc::DesktopCapturer::Callback { |
74 public: | 108 public: |
75 Worker(base::WeakPtr<NativeDesktopMediaList> media_list, | 109 Worker(base::WeakPtr<NativeDesktopMediaList> media_list, |
76 scoped_ptr<webrtc::ScreenCapturer> screen_capturer, | 110 scoped_ptr<webrtc::ScreenCapturer> screen_capturer, |
77 scoped_ptr<webrtc::WindowCapturer> window_capturer); | 111 scoped_ptr<webrtc::WindowCapturer> window_capturer); |
78 ~Worker() override; | 112 ~Worker() override; |
79 | 113 |
80 void Refresh(const gfx::Size& thumbnail_size, | 114 void Refresh(const gfx::Size& thumbnail_size, |
81 content::DesktopMediaID::Id view_dialog_id); | 115 DesktopMediaID::Id view_dialog_id, |
116 NativeAuraIdMap native_aura_id_map); | |
82 | 117 |
83 private: | 118 private: |
84 typedef std::map<DesktopMediaID, uint32_t> ImageHashesMap; | 119 typedef std::map<DesktopMediaID, uint32_t> ImageHashesMap; |
85 | 120 |
86 // webrtc::DesktopCapturer::Callback interface. | 121 // webrtc::DesktopCapturer::Callback interface. |
87 void OnCaptureCompleted(webrtc::DesktopFrame* frame) override; | 122 void OnCaptureCompleted(webrtc::DesktopFrame* frame) override; |
88 | 123 |
89 base::WeakPtr<NativeDesktopMediaList> media_list_; | 124 base::WeakPtr<NativeDesktopMediaList> media_list_; |
90 | 125 |
91 scoped_ptr<webrtc::ScreenCapturer> screen_capturer_; | 126 scoped_ptr<webrtc::ScreenCapturer> screen_capturer_; |
(...skipping 16 matching lines...) Expand all Loading... | |
108 if (screen_capturer_) | 143 if (screen_capturer_) |
109 screen_capturer_->Start(this); | 144 screen_capturer_->Start(this); |
110 if (window_capturer_) | 145 if (window_capturer_) |
111 window_capturer_->Start(this); | 146 window_capturer_->Start(this); |
112 } | 147 } |
113 | 148 |
114 NativeDesktopMediaList::Worker::~Worker() {} | 149 NativeDesktopMediaList::Worker::~Worker() {} |
115 | 150 |
116 void NativeDesktopMediaList::Worker::Refresh( | 151 void NativeDesktopMediaList::Worker::Refresh( |
117 const gfx::Size& thumbnail_size, | 152 const gfx::Size& thumbnail_size, |
118 content::DesktopMediaID::Id view_dialog_id) { | 153 DesktopMediaID::Id view_dialog_id, |
154 NativeAuraIdMap native_aura_id_map) { | |
119 std::vector<SourceDescription> sources; | 155 std::vector<SourceDescription> sources; |
156 std::vector<DesktopMediaID> aura_media_ids; | |
120 | 157 |
121 if (screen_capturer_) { | 158 if (screen_capturer_) { |
122 webrtc::ScreenCapturer::ScreenList screens; | 159 webrtc::ScreenCapturer::ScreenList screens; |
123 if (screen_capturer_->GetScreenList(&screens)) { | 160 if (screen_capturer_->GetScreenList(&screens)) { |
124 bool mutiple_screens = screens.size() > 1; | 161 bool mutiple_screens = screens.size() > 1; |
125 base::string16 title; | 162 base::string16 title; |
126 for (size_t i = 0; i < screens.size(); ++i) { | 163 for (size_t i = 0; i < screens.size(); ++i) { |
127 if (mutiple_screens) { | 164 if (mutiple_screens) { |
128 title = l10n_util::GetStringFUTF16Int( | 165 title = l10n_util::GetStringFUTF16Int( |
129 IDS_DESKTOP_MEDIA_PICKER_MULTIPLE_SCREEN_NAME, | 166 IDS_DESKTOP_MEDIA_PICKER_MULTIPLE_SCREEN_NAME, |
130 static_cast<int>(i + 1)); | 167 static_cast<int>(i + 1)); |
131 } else { | 168 } else { |
132 title = l10n_util::GetStringUTF16( | 169 title = l10n_util::GetStringUTF16( |
133 IDS_DESKTOP_MEDIA_PICKER_SINGLE_SCREEN_NAME); | 170 IDS_DESKTOP_MEDIA_PICKER_SINGLE_SCREEN_NAME); |
134 } | 171 } |
135 sources.push_back(SourceDescription(DesktopMediaID( | 172 sources.push_back(SourceDescription(DesktopMediaID( |
136 DesktopMediaID::TYPE_SCREEN, screens[i].id), title)); | 173 DesktopMediaID::TYPE_SCREEN, screens[i].id), title)); |
137 } | 174 } |
138 } | 175 } |
139 } | 176 } |
140 | 177 |
141 if (window_capturer_) { | 178 if (window_capturer_) { |
142 webrtc::WindowCapturer::WindowList windows; | 179 webrtc::WindowCapturer::WindowList windows; |
143 if (window_capturer_->GetWindowList(&windows)) { | 180 if (window_capturer_->GetWindowList(&windows)) { |
144 for (webrtc::WindowCapturer::WindowList::iterator it = windows.begin(); | 181 for (webrtc::WindowCapturer::WindowList::iterator it = windows.begin(); |
145 it != windows.end(); ++it) { | 182 it != windows.end(); ++it) { |
146 // Skip the picker dialog window. | 183 // Skip the picker dialog window. |
147 if (it->id != view_dialog_id) { | 184 if (it->id != view_dialog_id) { |
148 sources.push_back(SourceDescription( | 185 DesktopMediaID media_id(DesktopMediaID::TYPE_WINDOW, it->id); |
149 DesktopMediaID(DesktopMediaID::TYPE_WINDOW, it->id), | 186 #if defined(USE_AURA) |
150 base::UTF8ToUTF16(it->title))); | 187 // Associate aura id with native id. |
188 auto aura_id = native_aura_id_map.find(media_id.id); | |
189 if (aura_id != native_aura_id_map.end()) { | |
190 media_id.aura_id = aura_id->second; | |
191 aura_media_ids.push_back(media_id); | |
192 } | |
193 #endif | |
194 sources.push_back( | |
195 SourceDescription(media_id, base::UTF8ToUTF16(it->title))); | |
151 } | 196 } |
152 } | 197 } |
153 } | 198 } |
154 } | 199 } |
200 | |
155 // Update list of windows before updating thumbnails. | 201 // Update list of windows before updating thumbnails. |
156 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, | 202 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, |
157 base::Bind(&NativeDesktopMediaList::UpdateSourcesList, | 203 base::Bind(&NativeDesktopMediaList::UpdateSourcesList, |
158 media_list_, sources)); | 204 media_list_, sources)); |
159 | 205 |
160 ImageHashesMap new_image_hashes; | 206 ImageHashesMap new_image_hashes; |
161 | 207 |
162 // Get a thumbnail for each source. | 208 // Get a thumbnail for each source. |
163 for (size_t i = 0; i < sources.size(); ++i) { | 209 for (size_t i = 0; i < sources.size(); ++i) { |
164 SourceDescription& source = sources[i]; | 210 SourceDescription& source = sources[i]; |
211 | |
165 switch (source.id.type) { | 212 switch (source.id.type) { |
166 case DesktopMediaID::TYPE_SCREEN: | 213 case DesktopMediaID::TYPE_SCREEN: |
167 if (!screen_capturer_->SelectScreen(source.id.id)) | 214 if (!screen_capturer_->SelectScreen(source.id.id)) |
168 continue; | 215 continue; |
169 screen_capturer_->Capture(webrtc::DesktopRegion()); | 216 screen_capturer_->Capture(webrtc::DesktopRegion()); |
170 break; | 217 break; |
171 | 218 |
172 case DesktopMediaID::TYPE_WINDOW: | 219 case DesktopMediaID::TYPE_WINDOW: |
220 #if defined(USE_AURA) | |
221 // Aura window thumbmail capture is skipped here. It will be done | |
222 // asynchronously in the UI thread. | |
223 if (source.id.aura_id > DesktopMediaID::kNullId) | |
224 continue; | |
225 #endif | |
173 if (!window_capturer_->SelectWindow(source.id.id)) | 226 if (!window_capturer_->SelectWindow(source.id.id)) |
174 continue; | 227 continue; |
175 window_capturer_->Capture(webrtc::DesktopRegion()); | 228 window_capturer_->Capture(webrtc::DesktopRegion()); |
176 break; | 229 break; |
177 | 230 |
178 default: | 231 default: |
179 NOTREACHED(); | 232 NOTREACHED(); |
180 } | 233 } |
181 | 234 |
182 // Expect that DesktopCapturer to always captures frames synchronously. | 235 // Expect that DesktopCapturer to always captures frames synchronously. |
183 // |current_frame_| may be NULL if capture failed (e.g. because window has | 236 // |current_frame_| may be NULL if capture failed (e.g. because window has |
184 // been closed). | 237 // been closed). |
185 if (current_frame_) { | 238 if (current_frame_) { |
186 uint32_t frame_hash = GetFrameHash(current_frame_.get()); | 239 uint32_t frame_hash = GetFrameHash(current_frame_.get()); |
187 new_image_hashes[source.id] = frame_hash; | 240 new_image_hashes[source.id] = frame_hash; |
188 | 241 |
189 // Scale the image only if it has changed. | 242 // Scale the image only if it has changed. |
190 ImageHashesMap::iterator it = image_hashes_.find(source.id); | 243 ImageHashesMap::iterator it = image_hashes_.find(source.id); |
191 if (it == image_hashes_.end() || it->second != frame_hash) { | 244 if (it == image_hashes_.end() || it->second != frame_hash) { |
192 gfx::ImageSkia thumbnail = | 245 gfx::ImageSkia thumbnail = |
193 ScaleDesktopFrame(std::move(current_frame_), thumbnail_size); | 246 ScaleDesktopFrame(std::move(current_frame_), thumbnail_size); |
194 BrowserThread::PostTask( | 247 BrowserThread::PostTask( |
195 BrowserThread::UI, FROM_HERE, | 248 BrowserThread::UI, FROM_HERE, |
196 base::Bind(&NativeDesktopMediaList::OnSourceThumbnail, media_list_, | 249 base::Bind(&NativeDesktopMediaList::OnSourceThumbnailCaptured, |
197 i, thumbnail)); | 250 media_list_, i, thumbnail)); |
198 } | 251 } |
199 } | 252 } |
200 } | 253 } |
201 | 254 |
202 image_hashes_.swap(new_image_hashes); | 255 image_hashes_.swap(new_image_hashes); |
203 | 256 |
257 // Aura thumbnail captures have to be done in UI thread. After they are done, | |
258 // a refresh will be scheduled. | |
204 BrowserThread::PostTask( | 259 BrowserThread::PostTask( |
205 BrowserThread::UI, FROM_HERE, | 260 BrowserThread::UI, FROM_HERE, |
206 base::Bind(&NativeDesktopMediaList::ScheduleNextRefresh, media_list_)); | 261 base::Bind( |
262 &NativeDesktopMediaList::FinishRefreshOnUiThreadAndScheduleNext, | |
263 media_list_, aura_media_ids)); | |
207 } | 264 } |
208 | 265 |
209 void NativeDesktopMediaList::Worker::OnCaptureCompleted( | 266 void NativeDesktopMediaList::Worker::OnCaptureCompleted( |
210 webrtc::DesktopFrame* frame) { | 267 webrtc::DesktopFrame* frame) { |
211 current_frame_.reset(frame); | 268 current_frame_.reset(frame); |
212 } | 269 } |
213 | 270 |
214 NativeDesktopMediaList::NativeDesktopMediaList( | 271 NativeDesktopMediaList::NativeDesktopMediaList( |
215 scoped_ptr<webrtc::ScreenCapturer> screen_capturer, | 272 scoped_ptr<webrtc::ScreenCapturer> screen_capturer, |
216 scoped_ptr<webrtc::WindowCapturer> window_capturer) | 273 scoped_ptr<webrtc::WindowCapturer> window_capturer) |
217 : DesktopMediaListBase( | 274 : DesktopMediaListBase( |
218 base::TimeDelta::FromMilliseconds(kDefaultUpdatePeriod)), | 275 base::TimeDelta::FromMilliseconds(kDefaultUpdatePeriod)), |
219 weak_factory_(this) { | 276 weak_factory_(this) { |
220 base::SequencedWorkerPool* worker_pool = BrowserThread::GetBlockingPool(); | 277 base::SequencedWorkerPool* worker_pool = BrowserThread::GetBlockingPool(); |
221 capture_task_runner_ = worker_pool->GetSequencedTaskRunner( | 278 capture_task_runner_ = worker_pool->GetSequencedTaskRunner( |
222 worker_pool->GetSequenceToken()); | 279 worker_pool->GetSequenceToken()); |
223 | 280 |
224 worker_.reset(new Worker(weak_factory_.GetWeakPtr(), | 281 worker_.reset(new Worker(weak_factory_.GetWeakPtr(), |
225 std::move(screen_capturer), | 282 std::move(screen_capturer), |
226 std::move(window_capturer))); | 283 std::move(window_capturer))); |
227 } | 284 } |
228 | 285 |
229 NativeDesktopMediaList::~NativeDesktopMediaList() { | 286 NativeDesktopMediaList::~NativeDesktopMediaList() { |
230 capture_task_runner_->DeleteSoon(FROM_HERE, worker_.release()); | 287 capture_task_runner_->DeleteSoon(FROM_HERE, worker_.release()); |
231 } | 288 } |
232 | 289 |
233 void NativeDesktopMediaList::Refresh() { | 290 void NativeDesktopMediaList::Refresh() { |
291 NativeAuraIdMap native_aura_id_map; | |
292 #if defined(USE_AURA) | |
293 native_aura_id_map = GetBrowserNativeAuraIdMap(); | |
294 pending_aura_capture_requests_ = 0; | |
295 new_aura_thumbnail_hashes_.clear(); | |
296 #endif | |
297 | |
234 capture_task_runner_->PostTask( | 298 capture_task_runner_->PostTask( |
235 FROM_HERE, base::Bind(&Worker::Refresh, base::Unretained(worker_.get()), | 299 FROM_HERE, |
236 thumbnail_size_, view_dialog_id_.id)); | 300 base::Bind(&Worker::Refresh, base::Unretained(worker_.get()), |
301 thumbnail_size_, view_dialog_id_.id, native_aura_id_map)); | |
237 } | 302 } |
238 | 303 |
239 void NativeDesktopMediaList::OnSourceThumbnail(int index, | 304 void NativeDesktopMediaList::OnSourceThumbnailCaptured( |
240 const gfx::ImageSkia& image) { | 305 int index, |
306 const gfx::ImageSkia& image) { | |
241 UpdateSourceThumbnail(GetSource(index).id, image); | 307 UpdateSourceThumbnail(GetSource(index).id, image); |
242 } | 308 } |
309 | |
310 void NativeDesktopMediaList::FinishRefreshOnUiThreadAndScheduleNext( | |
311 const std::vector<DesktopMediaID>& aura_ids) { | |
312 // Schedule a refresh here when there is no aura thumbanil capture or schedule | |
313 // a refresh in OnAuraThumbnailCaptured() after all aura thumbnails are | |
314 // captured. | |
315 if (aura_ids.size() == 0) { | |
316 ScheduleNextRefresh(); | |
317 return; | |
318 } | |
319 | |
320 #if defined(USE_AURA) | |
321 DCHECK(pending_aura_capture_requests_ == 0); | |
Sergey Ulanov
2016/03/12 01:01:20
nit: DCHECK_EQ
GeorgeZ
2016/03/14 16:49:03
Done.
| |
322 for (const auto& aura_id : aura_ids) { | |
323 CaptureAuraWindowThumbnail(aura_id); | |
324 } | |
325 #endif | |
326 } | |
327 | |
328 #if defined(USE_AURA) | |
329 | |
330 void NativeDesktopMediaList::CaptureAuraWindowThumbnail(DesktopMediaID id) { | |
331 gfx::NativeWindow window = DesktopMediaID::GetAuraWindowById(id); | |
332 if (!window) | |
333 return; | |
334 | |
335 gfx::Rect window_rect(window->bounds().width(), window->bounds().height()); | |
336 gfx::Rect scaled_rect = media::ComputeLetterboxRegion( | |
337 gfx::Rect(thumbnail_size_), window_rect.size()); | |
338 | |
339 pending_aura_capture_requests_++; | |
340 ui::GrabWindowSnapshotAndScaleAsync( | |
341 window, window_rect, scaled_rect.size(), BrowserThread::GetBlockingPool(), | |
342 base::Bind(&NativeDesktopMediaList::OnAuraThumbnailCaptured, | |
343 weak_factory_.GetWeakPtr(), id)); | |
344 } | |
345 | |
346 void NativeDesktopMediaList::OnAuraThumbnailCaptured(DesktopMediaID id, | |
347 const gfx::Image& image) { | |
348 if (!image.IsEmpty()) { | |
349 // Only new or changed thumbnail need update. | |
350 new_aura_thumbnail_hashes_[id] = GetImageHash(image); | |
351 if (!previous_aura_thumbnail_hashes_.count(id) || | |
352 previous_aura_thumbnail_hashes_[id] != new_aura_thumbnail_hashes_[id]) { | |
353 UpdateSourceThumbnail(id, image.AsImageSkia()); | |
354 } | |
355 } | |
356 | |
357 // After all aura windows are processed, schedule next refresh; | |
358 pending_aura_capture_requests_--; | |
Sergey Ulanov
2016/03/12 01:01:20
DCHECK_GT(pending_aura_capture_requests_, 0)
GeorgeZ
2016/03/14 16:49:03
Done.
| |
359 if (pending_aura_capture_requests_ == 0) { | |
360 previous_aura_thumbnail_hashes_ = std::move(new_aura_thumbnail_hashes_); | |
361 ScheduleNextRefresh(); | |
362 } | |
363 } | |
364 | |
365 #endif // defined(USE_AURA) | |
OLD | NEW |