screen_capturer_mac.mm (22598B)
1 /* 2 * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved. 3 * 4 * Use of this source code is governed by a BSD-style license 5 * that can be found in the LICENSE file in the root of the source 6 * tree. An additional intellectual property rights grant can be found 7 * in the file PATENTS. All contributing project authors may 8 * be found in the AUTHORS file in the root of the source tree. 9 */ 10 11 #include <utility> 12 13 #include "modules/desktop_capture/mac/screen_capturer_mac.h" 14 15 #include "modules/desktop_capture/mac/desktop_frame_provider.h" 16 #include "modules/desktop_capture/mac/window_list_utils.h" 17 #include "rtc_base/checks.h" 18 #include "rtc_base/logging.h" 19 #include "rtc_base/time_utils.h" 20 #include "rtc_base/trace_event.h" 21 #include "sdk/objc/helpers/scoped_cftyperef.h" 22 23 namespace webrtc { 24 25 namespace { 26 27 // Scales all coordinates of a rect by a specified factor. 28 DesktopRect ScaleAndRoundCGRect(const CGRect& rect, float scale) { 29 return DesktopRect::MakeLTRB( 30 static_cast<int>(floor(rect.origin.x * scale)), 31 static_cast<int>(floor(rect.origin.y * scale)), 32 static_cast<int>(ceil((rect.origin.x + rect.size.width) * scale)), 33 static_cast<int>(ceil((rect.origin.y + rect.size.height) * scale))); 34 } 35 36 // Copy pixels in the `rect` from `src_place` to `dest_plane`. `rect` should be 37 // relative to the origin of `src_plane` and `dest_plane`. 38 void CopyRect(const uint8_t* src_plane, 39 int src_plane_stride, 40 uint8_t* dest_plane, 41 int dest_plane_stride, 42 int bytes_per_pixel, 43 const DesktopRect& rect) { 44 // Get the address of the starting point. 45 const int src_y_offset = src_plane_stride * rect.top(); 46 const int dest_y_offset = dest_plane_stride * rect.top(); 47 const int x_offset = bytes_per_pixel * rect.left(); 48 src_plane += src_y_offset + x_offset; 49 dest_plane += dest_y_offset + x_offset; 50 51 // Copy pixels in the rectangle line by line. 52 const int bytes_per_line = bytes_per_pixel * rect.width(); 53 const int height = rect.height(); 54 for (int i = 0; i < height; ++i) { 55 memcpy(dest_plane, src_plane, bytes_per_line); 56 src_plane += src_plane_stride; 57 dest_plane += dest_plane_stride; 58 } 59 } 60 61 // Returns an array of CGWindowID for all the on-screen windows except 62 // `window_to_exclude`, or NULL if the window is not found or it fails. The 63 // caller should release the returned CFArrayRef. 64 CFArrayRef CreateWindowListWithExclusion(CGWindowID window_to_exclude) { 65 if (!window_to_exclude) return nullptr; 66 67 CFArrayRef all_windows = CGWindowListCopyWindowInfo( 68 kCGWindowListOptionOnScreenOnly, kCGNullWindowID); 69 if (!all_windows) return nullptr; 70 71 CFMutableArrayRef returned_array = 72 CFArrayCreateMutable(nullptr, CFArrayGetCount(all_windows), nullptr); 73 74 bool found = false; 75 for (CFIndex i = 0; i < CFArrayGetCount(all_windows); ++i) { 76 CFDictionaryRef window = reinterpret_cast<CFDictionaryRef>( 77 CFArrayGetValueAtIndex(all_windows, i)); 78 79 CGWindowID id = GetWindowId(window); 80 if (id == window_to_exclude) { 81 found = true; 82 continue; 83 } 84 CFArrayAppendValue(returned_array, reinterpret_cast<void*>(id)); 85 } 86 CFRelease(all_windows); 87 88 if (!found) { 89 CFRelease(returned_array); 90 returned_array = nullptr; 91 } 92 return returned_array; 93 } 94 95 // Returns the bounds of `window` in physical pixels, enlarged by a small amount 96 // on four edges to take account of the border/shadow effects. 97 DesktopRect GetExcludedWindowPixelBounds(CGWindowID window, 98 float dip_to_pixel_scale) { 99 // The amount of pixels to add to the actual window bounds to take into 100 // account of the border/shadow effects. 101 static const int kBorderEffectSize = 20; 102 CGRect rect; 103 CGWindowID ids[1]; 104 ids[0] = window; 105 106 CFArrayRef window_id_array = 107 CFArrayCreate(nullptr, reinterpret_cast<const void**>(&ids), 1, nullptr); 108 CFArrayRef window_array = 109 CGWindowListCreateDescriptionFromArray(window_id_array); 110 111 if (CFArrayGetCount(window_array) > 0) { 112 CFDictionaryRef win = reinterpret_cast<CFDictionaryRef>( 113 CFArrayGetValueAtIndex(window_array, 0)); 114 CFDictionaryRef bounds_ref = reinterpret_cast<CFDictionaryRef>( 115 CFDictionaryGetValue(win, kCGWindowBounds)); 116 CGRectMakeWithDictionaryRepresentation(bounds_ref, &rect); 117 } 118 119 CFRelease(window_id_array); 120 CFRelease(window_array); 121 122 rect.origin.x -= kBorderEffectSize; 123 rect.origin.y -= kBorderEffectSize; 124 rect.size.width += kBorderEffectSize * 2; 125 rect.size.height += kBorderEffectSize * 2; 126 // `rect` is in DIP, so convert to physical pixels. 127 return ScaleAndRoundCGRect(rect, dip_to_pixel_scale); 128 } 129 130 // Create an image of the given region using the given `window_list`. 131 // `pixel_bounds` should be in the primary display's coordinate in physical 132 // pixels. 133 webrtc::ScopedCFTypeRef<CGImageRef> CreateExcludedWindowRegionImage( 134 const DesktopRect& pixel_bounds, 135 float dip_to_pixel_scale, 136 CFArrayRef window_list) { 137 CGRect window_bounds; 138 // The origin is in DIP while the size is in physical pixels. That's what 139 // CGWindowListCreateImageFromArray expects. 140 window_bounds.origin.x = pixel_bounds.left() / dip_to_pixel_scale; 141 window_bounds.origin.y = pixel_bounds.top() / dip_to_pixel_scale; 142 window_bounds.size.width = pixel_bounds.width(); 143 window_bounds.size.height = pixel_bounds.height(); 144 145 return webrtc::ScopedCFTypeRef<CGImageRef>(CGWindowListCreateImageFromArray( 146 window_bounds, window_list, kCGWindowImageDefault)); 147 } 148 149 } // namespace 150 151 ScreenCapturerMac::ScreenCapturerMac( 152 webrtc::scoped_refptr<DesktopConfigurationMonitor> desktop_config_monitor, 153 bool detect_updated_region, 154 bool allow_iosurface) 155 : detect_updated_region_(detect_updated_region), 156 desktop_config_monitor_(desktop_config_monitor), 157 desktop_frame_provider_(allow_iosurface) { 158 RTC_LOG(LS_INFO) << "Allow IOSurface: " << allow_iosurface; 159 thread_checker_.Detach(); 160 } 161 162 ScreenCapturerMac::~ScreenCapturerMac() { 163 RTC_DCHECK(thread_checker_.IsCurrent()); 164 ReleaseBuffers(); 165 UnregisterRefreshAndMoveHandlers(); 166 } 167 168 bool ScreenCapturerMac::Init() { 169 TRACE_EVENT0("webrtc", "ScreenCapturerMac::Init"); 170 desktop_config_ = desktop_config_monitor_->desktop_configuration(); 171 return true; 172 } 173 174 void ScreenCapturerMac::ReleaseBuffers() { 175 // The buffers might be in use by the encoder, so don't delete them here. 176 // Instead, mark them as "needs update"; next time the buffers are used by 177 // the capturer, they will be recreated if necessary. 178 queue_.Reset(); 179 } 180 181 void ScreenCapturerMac::Start(Callback* callback) { 182 RTC_DCHECK(thread_checker_.IsCurrent()); 183 RTC_DCHECK(!callback_); 184 RTC_DCHECK(callback); 185 TRACE_EVENT_INSTANT1("webrtc", 186 "ScreenCapturermac::Start", 187 TRACE_EVENT_SCOPE_GLOBAL, 188 "target display id ", 189 current_display_); 190 191 callback_ = callback; 192 update_screen_configuration_ = false; 193 // Start and operate CGDisplayStream handler all from capture thread. 194 if (!RegisterRefreshAndMoveHandlers()) { 195 RTC_LOG(LS_ERROR) << "Failed to register refresh and move handlers."; 196 callback_->OnCaptureResult(Result::ERROR_PERMANENT, nullptr); 197 return; 198 } 199 ScreenConfigurationChanged(); 200 } 201 202 void ScreenCapturerMac::CaptureFrame() { 203 RTC_DCHECK(thread_checker_.IsCurrent()); 204 TRACE_EVENT0("webrtc", "creenCapturerMac::CaptureFrame"); 205 int64_t capture_start_time_nanos = TimeNanos(); 206 207 queue_.MoveToNextFrame(); 208 if (queue_.current_frame() && queue_.current_frame()->IsShared()) { 209 RTC_DLOG(LS_WARNING) << "Overwriting frame that is still shared."; 210 } 211 212 MacDesktopConfiguration new_config = 213 desktop_config_monitor_->desktop_configuration(); 214 if (update_screen_configuration_ || !desktop_config_.Equals(new_config)) { 215 update_screen_configuration_ = false; 216 desktop_config_ = new_config; 217 // If the display configuraiton has changed then refresh capturer data 218 // structures. Occasionally, the refresh and move handlers are lost when 219 // the screen mode changes, so re-register them here. 220 UnregisterRefreshAndMoveHandlers(); 221 if (!RegisterRefreshAndMoveHandlers()) { 222 RTC_LOG(LS_ERROR) << "Failed to register refresh and move handlers."; 223 callback_->OnCaptureResult(Result::ERROR_PERMANENT, nullptr); 224 return; 225 } 226 ScreenConfigurationChanged(); 227 } 228 229 // When screen is zoomed in/out, OSX only updates the part of Rects currently 230 // displayed on screen, with relative location to current top-left on screen. 231 // This will cause problems when we copy the dirty regions to the captured 232 // image. So we invalidate the whole screen to copy all the screen contents. 233 // With CGI method, the zooming will be ignored and the whole screen contents 234 // will be captured as before. 235 // With IOSurface method, the zoomed screen contents will be captured. 236 if (UAZoomEnabled()) { 237 helper_.InvalidateScreen(screen_pixel_bounds_.size()); 238 } 239 240 DesktopRegion region; 241 helper_.TakeInvalidRegion(®ion); 242 243 // If the current buffer is from an older generation then allocate a new one. 244 // Note that we can't reallocate other buffers at this point, since the caller 245 // may still be reading from them. 246 if (!queue_.current_frame()) 247 queue_.ReplaceCurrentFrame(SharedDesktopFrame::Wrap(CreateFrame())); 248 249 DesktopFrame* current_frame = queue_.current_frame(); 250 251 if (!CgBlit(*current_frame, region)) { 252 callback_->OnCaptureResult(Result::ERROR_PERMANENT, nullptr); 253 return; 254 } 255 std::unique_ptr<DesktopFrame> new_frame = queue_.current_frame()->Share(); 256 if (detect_updated_region_) { 257 *new_frame->mutable_updated_region() = region; 258 } else { 259 new_frame->mutable_updated_region()->AddRect( 260 DesktopRect::MakeSize(new_frame->size())); 261 } 262 263 if (current_display_) { 264 const MacDisplayConfiguration* config = 265 desktop_config_.FindDisplayConfigurationById(current_display_); 266 if (config) { 267 new_frame->set_top_left(config->bounds.top_left().subtract( 268 desktop_config_.bounds.top_left())); 269 } 270 } 271 272 helper_.set_size_most_recent(new_frame->size()); 273 274 new_frame->set_capture_time_ms( 275 (webrtc::TimeNanos() - capture_start_time_nanos) / 276 webrtc::kNumNanosecsPerMillisec); 277 callback_->OnCaptureResult(Result::SUCCESS, std::move(new_frame)); 278 } 279 280 void ScreenCapturerMac::SetExcludedWindow(WindowId window) { 281 excluded_window_ = window; 282 } 283 284 bool ScreenCapturerMac::GetSourceList(SourceList* screens) { 285 RTC_DCHECK(screens->size() == 0); 286 287 for (MacDisplayConfigurations::iterator it = desktop_config_.displays.begin(); 288 it != desktop_config_.displays.end(); 289 ++it) { 290 Source value = {it->id, 0, std::string()}; 291 screens->push_back(value); 292 } 293 return true; 294 } 295 296 bool ScreenCapturerMac::SelectSource(SourceId id) { 297 if (id == kFullDesktopScreenId) { 298 current_display_ = 0; 299 } else { 300 const MacDisplayConfiguration* config = 301 desktop_config_.FindDisplayConfigurationById( 302 static_cast<CGDirectDisplayID>(id)); 303 if (!config) return false; 304 current_display_ = config->id; 305 } 306 307 ScreenConfigurationChanged(); 308 return true; 309 } 310 311 bool ScreenCapturerMac::CgBlit(const DesktopFrame& frame, 312 const DesktopRegion& region) { 313 // If not all screen region is dirty, copy the entire contents of the previous 314 // capture buffer, to capture over. 315 if (queue_.previous_frame() && 316 !region.Equals(DesktopRegion(screen_pixel_bounds_))) { 317 memcpy(frame.data(), 318 queue_.previous_frame()->data(), 319 frame.stride() * frame.size().height()); 320 } 321 322 MacDisplayConfigurations displays_to_capture; 323 if (current_display_) { 324 // Capturing a single screen. Note that the screen id may change when 325 // screens are added or removed. 326 const MacDisplayConfiguration* config = 327 desktop_config_.FindDisplayConfigurationById(current_display_); 328 if (config) { 329 displays_to_capture.push_back(*config); 330 } else { 331 RTC_LOG(LS_ERROR) << "The selected screen cannot be found for capturing."; 332 return false; 333 } 334 } else { 335 // Capturing the whole desktop. 336 displays_to_capture = desktop_config_.displays; 337 } 338 339 // Create the window list once for all displays. 340 CFArrayRef window_list = CreateWindowListWithExclusion(excluded_window_); 341 342 for (size_t i = 0; i < displays_to_capture.size(); ++i) { 343 const MacDisplayConfiguration& display_config = displays_to_capture[i]; 344 345 // Capturing mixed-DPI on one surface is hard, so we only return displays 346 // that match the "primary" display's DPI. The primary display is always 347 // the first in the list. 348 if (i > 0 && 349 display_config.dip_to_pixel_scale != 350 displays_to_capture[0].dip_to_pixel_scale) { 351 continue; 352 } 353 // Determine the display's position relative to the desktop, in pixels. 354 DesktopRect display_bounds = display_config.pixel_bounds; 355 display_bounds.Translate(-screen_pixel_bounds_.left(), 356 -screen_pixel_bounds_.top()); 357 358 // Determine which parts of the blit region, if any, lay within the monitor. 359 DesktopRegion copy_region = region; 360 copy_region.IntersectWith(display_bounds); 361 if (copy_region.is_empty()) continue; 362 363 // Translate the region to be copied into display-relative coordinates. 364 copy_region.Translate(-display_bounds.left(), -display_bounds.top()); 365 366 DesktopRect excluded_window_bounds; 367 webrtc::ScopedCFTypeRef<CGImageRef> excluded_image; 368 if (excluded_window_ && window_list) { 369 // Get the region of the excluded window relative the primary display. 370 excluded_window_bounds = GetExcludedWindowPixelBounds( 371 excluded_window_, display_config.dip_to_pixel_scale); 372 excluded_window_bounds.IntersectWith(display_config.pixel_bounds); 373 374 // Create the image under the excluded window first, because it's faster 375 // than captuing the whole display. 376 if (!excluded_window_bounds.is_empty()) { 377 excluded_image = 378 CreateExcludedWindowRegionImage(excluded_window_bounds, 379 display_config.dip_to_pixel_scale, 380 window_list); 381 } 382 } 383 384 std::unique_ptr<DesktopFrame> frame_source = 385 desktop_frame_provider_.TakeLatestFrameForDisplay(display_config.id); 386 if (!frame_source) { 387 continue; 388 } 389 RTC_CHECK_EQ(frame_source->pixel_format(), frame.pixel_format()); 390 391 const uint8_t* display_base_address = frame_source->data(); 392 int src_bytes_per_row = frame_source->stride(); 393 RTC_DCHECK(display_base_address); 394 395 // `frame_source` size may be different from display_bounds in case the 396 // screen was resized recently. 397 copy_region.IntersectWith(frame_source->rect()); 398 399 // Copy the dirty region from the display buffer into our desktop buffer. 400 uint8_t* out_ptr = frame.GetFrameDataAtPos(display_bounds.top_left()); 401 for (DesktopRegion::Iterator it(copy_region); !it.IsAtEnd(); it.Advance()) { 402 CopyRect(display_base_address, 403 src_bytes_per_row, 404 out_ptr, 405 frame.stride(), 406 DesktopFrame::kBytesPerPixel, 407 it.rect()); 408 } 409 410 if (excluded_image) { 411 CGDataProviderRef provider = CGImageGetDataProvider(excluded_image.get()); 412 webrtc::ScopedCFTypeRef<CFDataRef> excluded_image_data( 413 CGDataProviderCopyData(provider)); 414 RTC_DCHECK(excluded_image_data); 415 display_base_address = CFDataGetBytePtr(excluded_image_data.get()); 416 src_bytes_per_row = CGImageGetBytesPerRow(excluded_image.get()); 417 418 // Translate the bounds relative to the desktop, because `frame` data 419 // starts from the desktop top-left corner. 420 DesktopRect window_bounds_relative_to_desktop(excluded_window_bounds); 421 window_bounds_relative_to_desktop.Translate(-screen_pixel_bounds_.left(), 422 -screen_pixel_bounds_.top()); 423 424 DesktopRect rect_to_copy = 425 DesktopRect::MakeSize(excluded_window_bounds.size()); 426 rect_to_copy.IntersectWith( 427 DesktopRect::MakeWH(CGImageGetWidth(excluded_image.get()), 428 CGImageGetHeight(excluded_image.get()))); 429 430 if (CGImageGetBitsPerPixel(excluded_image.get()) / 8 == 431 DesktopFrame::kBytesPerPixel) { 432 CopyRect(display_base_address, 433 src_bytes_per_row, 434 frame.GetFrameDataAtPos( 435 window_bounds_relative_to_desktop.top_left()), 436 frame.stride(), 437 DesktopFrame::kBytesPerPixel, 438 rect_to_copy); 439 } 440 } 441 } 442 if (window_list) CFRelease(window_list); 443 return true; 444 } 445 446 void ScreenCapturerMac::ScreenConfigurationChanged() { 447 if (current_display_) { 448 const MacDisplayConfiguration* config = 449 desktop_config_.FindDisplayConfigurationById(current_display_); 450 screen_pixel_bounds_ = config ? config->pixel_bounds : DesktopRect(); 451 dip_to_pixel_scale_ = config ? config->dip_to_pixel_scale : 1.0f; 452 } else { 453 screen_pixel_bounds_ = desktop_config_.pixel_bounds; 454 dip_to_pixel_scale_ = desktop_config_.dip_to_pixel_scale; 455 } 456 457 // Release existing buffers, which will be of the wrong size. 458 ReleaseBuffers(); 459 460 // Clear the dirty region, in case the display is down-sizing. 461 helper_.ClearInvalidRegion(); 462 463 // Re-mark the entire desktop as dirty. 464 helper_.InvalidateScreen(screen_pixel_bounds_.size()); 465 466 // Make sure the frame buffers will be reallocated. 467 queue_.Reset(); 468 } 469 470 bool ScreenCapturerMac::RegisterRefreshAndMoveHandlers() { 471 RTC_DCHECK(thread_checker_.IsCurrent()); 472 if (!desktop_frame_provider_.allow_iosurface()) { 473 return true; 474 } 475 476 desktop_config_ = desktop_config_monitor_->desktop_configuration(); 477 for (const auto& config : desktop_config_.displays) { 478 size_t pixel_width = config.pixel_bounds.width(); 479 size_t pixel_height = config.pixel_bounds.height(); 480 if (pixel_width == 0 || pixel_height == 0) continue; 481 CGDirectDisplayID display_id = config.id; 482 DesktopVector display_origin = config.pixel_bounds.top_left(); 483 484 CGDisplayStreamFrameAvailableHandler handler = ^( 485 CGDisplayStreamFrameStatus status, 486 uint64_t /* display_time */, 487 IOSurfaceRef frame_surface, 488 CGDisplayStreamUpdateRef updateRef) { 489 RTC_DCHECK(thread_checker_.IsCurrent()); 490 if (status == kCGDisplayStreamFrameStatusStopped) return; 491 492 // Only pay attention to frame updates. 493 if (status != kCGDisplayStreamFrameStatusFrameComplete) return; 494 495 size_t count = 0; 496 const CGRect* rects = CGDisplayStreamUpdateGetRects( 497 updateRef, kCGDisplayStreamUpdateDirtyRects, &count); 498 if (count != 0) { 499 // According to CGDisplayStream.h, it's safe to call 500 // CGDisplayStreamStop() from within the callback. 501 ScreenRefresh(display_id, count, rects, display_origin, frame_surface); 502 } 503 }; 504 505 webrtc::ScopedCFTypeRef<CFDictionaryRef> properties_dict( 506 CFDictionaryCreate(kCFAllocatorDefault, 507 (const void*[]){kCGDisplayStreamShowCursor}, 508 (const void*[]){kCFBooleanFalse}, 509 1, 510 &kCFTypeDictionaryKeyCallBacks, 511 &kCFTypeDictionaryValueCallBacks)); 512 513 CGDisplayStreamRef display_stream = 514 CGDisplayStreamCreate(display_id, 515 pixel_width, 516 pixel_height, 517 'BGRA', 518 properties_dict.get(), 519 handler); 520 521 if (display_stream) { 522 CGError error = CGDisplayStreamStart(display_stream); 523 if (error != kCGErrorSuccess) return false; 524 525 CFRunLoopSourceRef source = 526 CGDisplayStreamGetRunLoopSource(display_stream); 527 CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes); 528 display_streams_.push_back(display_stream); 529 } 530 } 531 532 return true; 533 } 534 535 void ScreenCapturerMac::UnregisterRefreshAndMoveHandlers() { 536 RTC_DCHECK(thread_checker_.IsCurrent()); 537 538 for (CGDisplayStreamRef stream : display_streams_) { 539 CFRunLoopSourceRef source = CGDisplayStreamGetRunLoopSource(stream); 540 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes); 541 CGDisplayStreamStop(stream); 542 CFRelease(stream); 543 } 544 display_streams_.clear(); 545 546 // Release obsolete io surfaces. 547 desktop_frame_provider_.Release(); 548 } 549 550 void ScreenCapturerMac::ScreenRefresh(CGDirectDisplayID display_id, 551 CGRectCount count, 552 const CGRect* rect_array, 553 DesktopVector display_origin, 554 IOSurfaceRef io_surface) { 555 if (screen_pixel_bounds_.is_empty()) ScreenConfigurationChanged(); 556 557 // The refresh rects are in display coordinates. We want to translate to 558 // framebuffer coordinates. If a specific display is being captured, then no 559 // change is necessary. If all displays are being captured, then we want to 560 // translate by the origin of the display. 561 DesktopVector translate_vector; 562 if (!current_display_) translate_vector = display_origin; 563 564 DesktopRegion region; 565 for (CGRectCount i = 0; i < count; ++i) { 566 // All rects are already in physical pixel coordinates. 567 DesktopRect rect = DesktopRect::MakeXYWH(rect_array[i].origin.x, 568 rect_array[i].origin.y, 569 rect_array[i].size.width, 570 rect_array[i].size.height); 571 572 rect.Translate(translate_vector); 573 574 region.AddRect(rect); 575 } 576 // Always having the latest iosurface before invalidating a region. 577 // See https://bugs.chromium.org/p/webrtc/issues/detail?id=8652 for details. 578 desktop_frame_provider_.InvalidateIOSurface( 579 display_id, 580 webrtc::ScopedCFTypeRef<IOSurfaceRef>(io_surface, 581 webrtc::RetainPolicy::RETAIN)); 582 helper_.InvalidateRegion(region); 583 } 584 585 std::unique_ptr<DesktopFrame> ScreenCapturerMac::CreateFrame() { 586 std::unique_ptr<DesktopFrame> frame( 587 new BasicDesktopFrame(screen_pixel_bounds_.size())); 588 frame->set_dpi(DesktopVector(kStandardDPI * dip_to_pixel_scale_, 589 kStandardDPI * dip_to_pixel_scale_)); 590 return frame; 591 } 592 593 } // namespace webrtc