Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 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 // Implements the Chrome Extensions Tab Capture API. | 5 // Implements the Chrome Extensions Tab Capture API. |
| 6 | 6 |
| 7 #include "chrome/browser/extensions/api/tab_capture/tab_capture_api.h" | 7 #include "chrome/browser/extensions/api/tab_capture/tab_capture_api.h" |
| 8 | 8 |
| 9 #include <algorithm> | |
| 9 #include <set> | 10 #include <set> |
| 10 #include <string> | 11 #include <string> |
| 11 #include <vector> | 12 #include <vector> |
| 12 | 13 |
| 13 #include "base/command_line.h" | 14 #include "base/command_line.h" |
| 14 #include "base/strings/stringprintf.h" | 15 #include "base/strings/stringprintf.h" |
| 15 #include "base/values.h" | 16 #include "base/values.h" |
| 17 #include "chrome/browser/extensions/api/tab_capture/offscreen_presentation.h" | |
| 16 #include "chrome/browser/extensions/api/tab_capture/tab_capture_registry.h" | 18 #include "chrome/browser/extensions/api/tab_capture/tab_capture_registry.h" |
| 17 #include "chrome/browser/extensions/extension_renderer_state.h" | 19 #include "chrome/browser/extensions/extension_renderer_state.h" |
| 18 #include "chrome/browser/profiles/profile.h" | 20 #include "chrome/browser/profiles/profile.h" |
| 19 #include "chrome/browser/sessions/session_tab_helper.h" | 21 #include "chrome/browser/sessions/session_tab_helper.h" |
| 20 #include "chrome/browser/ui/browser.h" | 22 #include "chrome/browser/ui/browser.h" |
| 21 #include "chrome/browser/ui/browser_finder.h" | 23 #include "chrome/browser/ui/browser_finder.h" |
| 22 #include "chrome/browser/ui/tabs/tab_strip_model.h" | 24 #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| 25 #include "chrome/common/chrome_switches.h" | |
| 23 #include "content/public/browser/render_frame_host.h" | 26 #include "content/public/browser/render_frame_host.h" |
| 24 #include "content/public/browser/render_process_host.h" | 27 #include "content/public/browser/render_process_host.h" |
| 28 #include "content/public/browser/web_contents.h" | |
| 25 #include "extensions/common/features/feature.h" | 29 #include "extensions/common/features/feature.h" |
| 26 #include "extensions/common/features/feature_provider.h" | 30 #include "extensions/common/features/feature_provider.h" |
| 27 #include "extensions/common/features/simple_feature.h" | 31 #include "extensions/common/features/simple_feature.h" |
| 28 #include "extensions/common/permissions/permissions_data.h" | 32 #include "extensions/common/permissions/permissions_data.h" |
| 29 #include "extensions/common/switches.h" | 33 #include "extensions/common/switches.h" |
| 30 | 34 |
| 31 using extensions::api::tab_capture::MediaStreamConstraint; | 35 using extensions::api::tab_capture::MediaStreamConstraint; |
| 32 | 36 |
| 33 namespace TabCapture = extensions::api::tab_capture; | 37 namespace TabCapture = extensions::api::tab_capture; |
| 34 namespace GetCapturedTabs = TabCapture::GetCapturedTabs; | 38 namespace GetCapturedTabs = TabCapture::GetCapturedTabs; |
| 35 | 39 |
| 36 namespace extensions { | 40 namespace extensions { |
| 37 namespace { | 41 namespace { |
| 38 | 42 |
| 39 const char kCapturingSameTab[] = "Cannot capture a tab with an active stream."; | 43 const char kCapturingSameTab[] = "Cannot capture a tab with an active stream."; |
| 40 const char kFindingTabError[] = "Error finding tab to capture."; | 44 const char kFindingTabError[] = "Error finding tab to capture."; |
| 41 const char kNoAudioOrVideo[] = "Capture failed. No audio or video requested."; | 45 const char kNoAudioOrVideo[] = "Capture failed. No audio or video requested."; |
| 42 const char kGrantError[] = | 46 const char kGrantError[] = |
| 43 "Extension has not been invoked for the current page (see activeTab " | 47 "Extension has not been invoked for the current page (see activeTab " |
| 44 "permission). Chrome pages cannot be captured."; | 48 "permission). Chrome pages cannot be captured."; |
| 45 | 49 |
| 50 const char kCapturePresentationApiNotEnabled[] = | |
| 51 "Experimental capturePresentation API is not enabled."; | |
| 52 const char kMissingTabCapturePermission[] = | |
| 53 "Extension manifest did not specify tabCapture permission."; | |
| 54 const char kInvalidPresentationUrl[] = | |
| 55 "Invalid/Missing/Malformatted starting URL for presentation."; | |
| 56 const char kMissingExtensionPage[] = | |
| 57 "Error finding extension page."; | |
| 58 const char kTooManyPresentations[] = | |
| 59 "Extension has already started too many presentations."; | |
| 60 const char kCapturingSamePresentation[] = | |
| 61 "Cannot capture the same presentation more than once."; | |
| 62 | |
| 46 // Keys/values for media stream constraints. | 63 // Keys/values for media stream constraints. |
| 47 const char kMediaStreamSource[] = "chromeMediaSource"; | 64 const char kMediaStreamSource[] = "chromeMediaSource"; |
| 48 const char kMediaStreamSourceId[] = "chromeMediaSourceId"; | 65 const char kMediaStreamSourceId[] = "chromeMediaSourceId"; |
| 49 const char kMediaStreamSourceTab[] = "tab"; | 66 const char kMediaStreamSourceTab[] = "tab"; |
| 50 | 67 |
| 51 // Tab Capture-specific video constraint to enable automatic resolution/rate | 68 // Tab Capture-specific video constraint to enable automatic resolution/rate |
| 52 // throttling mode in the capture pipeline. | 69 // throttling mode in the capture pipeline. |
| 53 const char kEnableAutoThrottlingKey[] = "enableAutoThrottling"; | 70 const char kEnableAutoThrottlingKey[] = "enableAutoThrottling"; |
| 54 | 71 |
| 72 bool OptionsSpecifyAudioOrVideo(const TabCapture::CaptureOptions& options) { | |
| 73 return (options.audio && *options.audio) || (options.video && *options.video); | |
| 74 } | |
| 75 | |
| 76 bool IsAcceptablePresentationUrl(const GURL& url) { | |
| 77 return url.is_valid() && (url.SchemeIsHTTPOrHTTPS() || url.SchemeIs("data")); | |
| 78 } | |
| 79 | |
| 80 void AddMediaStreamSourceOptions(TabCapture::CaptureOptions* options, | |
| 81 content::WebContents* target_contents) { | |
| 82 DCHECK(options); | |
| 83 DCHECK(target_contents); | |
| 84 | |
| 85 // Create a constraints vector. We will modify all the constraints in this | |
| 86 // vector to append our chrome specific constraints. | |
| 87 std::vector<MediaStreamConstraint*> constraints; | |
| 88 if (options->audio.get() && *options->audio.get()) { | |
|
Kevin M
2015/06/29 22:17:02
.get() isn't needed here or elsewhere in the funct
miu
2015/06/30 04:09:59
Done. In some places, it was still required.
| |
| 89 if (!options->audio_constraints.get()) | |
| 90 options->audio_constraints.reset(new MediaStreamConstraint); | |
| 91 constraints.push_back(options->audio_constraints.get()); | |
| 92 } | |
| 93 bool enable_auto_throttling = false; | |
| 94 if (options->video.get() && *options->video.get()) { | |
| 95 if (options->video_constraints.get()) { | |
| 96 // Check for the Tab Capture-specific video constraint for enabling | |
| 97 // automatic resolution/rate throttling mode in the capture pipeline. See | |
| 98 // implementation comments for content::WebContentsVideoCaptureDevice. | |
| 99 base::DictionaryValue& props = | |
| 100 options->video_constraints->mandatory.additional_properties; | |
| 101 if (!props.GetBooleanWithoutPathExpansion( | |
| 102 kEnableAutoThrottlingKey, &enable_auto_throttling)) { | |
| 103 enable_auto_throttling = false; | |
| 104 } | |
| 105 // Remove the key from the properties to avoid an "unrecognized | |
| 106 // constraint" error in the renderer. | |
| 107 props.RemoveWithoutPathExpansion(kEnableAutoThrottlingKey, nullptr); | |
| 108 } else { | |
| 109 options->video_constraints.reset(new MediaStreamConstraint); | |
| 110 } | |
| 111 constraints.push_back(options->video_constraints.get()); | |
| 112 } | |
| 113 | |
| 114 // Device id we use for Tab Capture. | |
| 115 content::RenderFrameHost* const main_frame = target_contents->GetMainFrame(); | |
| 116 // TODO(miu): We should instead use a "randomly generated device ID" scheme, | |
| 117 // like that employed by the desktop capture API. http://crbug.com/163100 | |
| 118 const std::string device_id = base::StringPrintf( | |
| 119 "web-contents-media-stream://%i:%i%s", | |
| 120 main_frame->GetProcess()->GetID(), | |
| 121 main_frame->GetRoutingID(), | |
| 122 enable_auto_throttling ? "?throttling=auto" : ""); | |
| 123 | |
| 124 // Append chrome specific tab constraints. | |
| 125 for (MediaStreamConstraint* msc : constraints) { | |
| 126 base::DictionaryValue* constraint = &msc->mandatory.additional_properties; | |
| 127 constraint->SetString(kMediaStreamSource, kMediaStreamSourceTab); | |
| 128 constraint->SetString(kMediaStreamSourceId, device_id); | |
| 129 } | |
| 130 } | |
| 131 | |
| 132 // Examine the minimum and maximum size constraints in |options| to determine | |
| 133 // the page size for an off-screen presentation. | |
| 134 gfx::Size DeterminePresentationSize(const TabCapture::CaptureOptions& options) { | |
|
Kevin M
2015/06/29 22:17:02
Could this be unit tested to ensure that all the c
miu
2015/06/30 04:09:59
Done. Made this function a static class member to
| |
| 135 static const int kDefaultWidth = 1280; | |
| 136 static const int kDefaultHeight = 720; | |
| 137 | |
| 138 if (!options.video_constraints) | |
| 139 return gfx::Size(kDefaultWidth, kDefaultHeight); | |
| 140 | |
| 141 gfx::Size min_size; | |
| 142 int width = -1; | |
| 143 int height = -1; | |
| 144 const base::DictionaryValue& mandatory_properties = | |
| 145 options.video_constraints->mandatory.additional_properties; | |
| 146 if (mandatory_properties.GetInteger("maxWidth", &width) && width >= 0 && | |
| 147 mandatory_properties.GetInteger("maxHeight", &height) && height >= 0) { | |
| 148 return gfx::Size(width, height); | |
| 149 } | |
| 150 if (mandatory_properties.GetInteger("minWidth", &width) && width >= 0 && | |
| 151 mandatory_properties.GetInteger("minHeight", &height) && height >= 0) { | |
| 152 min_size.SetSize(width, height); | |
| 153 } | |
| 154 | |
| 155 // Use optional size constraints if no mandatory ones were provided. | |
| 156 if (options.video_constraints->optional.get()) { | |
| 157 const base::DictionaryValue& optional_properties = | |
| 158 options.video_constraints->optional->additional_properties; | |
| 159 if (optional_properties.GetInteger("maxWidth", &width) && width >= 0 && | |
| 160 optional_properties.GetInteger("maxHeight", &height) && height >= 0) { | |
| 161 if (min_size.IsEmpty()) { | |
| 162 return gfx::Size(width, height); | |
| 163 } else { | |
| 164 return gfx::Size(std::max(width, min_size.width()), | |
| 165 std::max(height, min_size.height())); | |
| 166 } | |
| 167 } | |
| 168 if (min_size.IsEmpty() && | |
| 169 optional_properties.GetInteger("minWidth", &width) && width >= 0 && | |
| 170 optional_properties.GetInteger("minHeight", &height) && height >= 0) { | |
| 171 min_size.SetSize(width, height); | |
| 172 } | |
| 173 } | |
| 174 | |
| 175 // No maximum size was provided, so just return the default size bounded by | |
| 176 // the minimum size. | |
| 177 return gfx::Size(std::max(kDefaultWidth, min_size.width()), | |
| 178 std::max(kDefaultHeight, min_size.height())); | |
| 179 } | |
| 180 | |
| 55 } // namespace | 181 } // namespace |
| 56 | 182 |
| 57 // Whitelisted extensions that do not check for a browser action grant because | 183 // Whitelisted extensions that do not check for a browser action grant because |
| 58 // they provide API's. If there are additional extension ids that need | 184 // they provide API's. If there are additional extension ids that need |
| 59 // whitelisting and are *not* the Chromecast extension, add them to a new | 185 // whitelisting and are *not* the Chromecast extension, add them to a new |
| 60 // kWhitelist array. | 186 // kWhitelist array. |
| 61 const char* const kChromecastExtensionIds[] = { | 187 const char* const kChromecastExtensionIds[] = { |
| 62 "enhhojjnijigcajfphajepfemndkmdlo", // Dev | 188 "enhhojjnijigcajfphajepfemndkmdlo", // Dev |
| 63 "pkedcjkdefgpdelpbcmbmeomcjbeemfm", // Dogfood | 189 "pkedcjkdefgpdelpbcmbmeomcjbeemfm", // Dogfood |
| 64 "fmfcbgogabcbclcofgocippekhfcmgfj", // Staging | 190 "fmfcbgogabcbclcofgocippekhfcmgfj", // Staging |
| (...skipping 30 matching lines...) Expand all Loading... | |
| 95 SessionTabHelper::IdForTab(target_contents), | 221 SessionTabHelper::IdForTab(target_contents), |
| 96 APIPermission::kTabCaptureForTab) && | 222 APIPermission::kTabCaptureForTab) && |
| 97 base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( | 223 base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| 98 switches::kWhitelistedExtensionID) != extension_id && | 224 switches::kWhitelistedExtensionID) != extension_id && |
| 99 !SimpleFeature::IsIdInArray(extension_id, kChromecastExtensionIds, | 225 !SimpleFeature::IsIdInArray(extension_id, kChromecastExtensionIds, |
| 100 arraysize(kChromecastExtensionIds))) { | 226 arraysize(kChromecastExtensionIds))) { |
| 101 error_ = kGrantError; | 227 error_ = kGrantError; |
| 102 return false; | 228 return false; |
| 103 } | 229 } |
| 104 | 230 |
| 105 // Create a constraints vector. We will modify all the constraints in this | 231 if (!OptionsSpecifyAudioOrVideo(params->options)) { |
| 106 // vector to append our chrome specific constraints. | |
| 107 std::vector<MediaStreamConstraint*> constraints; | |
| 108 bool has_audio = params->options.audio.get() && *params->options.audio.get(); | |
| 109 bool has_video = params->options.video.get() && *params->options.video.get(); | |
| 110 | |
| 111 if (!has_audio && !has_video) { | |
| 112 error_ = kNoAudioOrVideo; | 232 error_ = kNoAudioOrVideo; |
| 113 return false; | 233 return false; |
| 114 } | 234 } |
| 115 | 235 |
| 116 if (has_audio) { | |
| 117 if (!params->options.audio_constraints.get()) | |
| 118 params->options.audio_constraints.reset(new MediaStreamConstraint); | |
| 119 | |
| 120 constraints.push_back(params->options.audio_constraints.get()); | |
| 121 } | |
| 122 | |
| 123 bool enable_auto_throttling = false; | |
| 124 if (has_video) { | |
| 125 if (params->options.video_constraints.get()) { | |
| 126 // Check for the Tab Capture-specific video constraint for enabling | |
| 127 // automatic resolution/rate throttling mode in the capture pipeline. See | |
| 128 // implementation comments for content::WebContentsVideoCaptureDevice. | |
| 129 base::DictionaryValue& props = | |
| 130 params->options.video_constraints->mandatory.additional_properties; | |
| 131 if (!props.GetBooleanWithoutPathExpansion( | |
| 132 kEnableAutoThrottlingKey, &enable_auto_throttling)) { | |
| 133 enable_auto_throttling = false; | |
| 134 } | |
| 135 // Remove the key from the properties to avoid an "unrecognized | |
| 136 // constraint" error in the renderer. | |
| 137 props.RemoveWithoutPathExpansion(kEnableAutoThrottlingKey, nullptr); | |
| 138 } else { | |
| 139 params->options.video_constraints.reset(new MediaStreamConstraint); | |
| 140 } | |
| 141 | |
| 142 constraints.push_back(params->options.video_constraints.get()); | |
| 143 } | |
| 144 | |
| 145 // Device id we use for Tab Capture. | |
| 146 content::RenderFrameHost* const main_frame = target_contents->GetMainFrame(); | |
| 147 // TODO(miu): We should instead use a "randomly generated device ID" scheme, | |
| 148 // like that employed by the desktop capture API. http://crbug.com/163100 | |
| 149 const std::string device_id = base::StringPrintf( | |
| 150 "web-contents-media-stream://%i:%i%s", | |
| 151 main_frame->GetProcess()->GetID(), | |
| 152 main_frame->GetRoutingID(), | |
| 153 enable_auto_throttling ? "?throttling=auto" : ""); | |
| 154 | |
| 155 // Append chrome specific tab constraints. | |
| 156 for (std::vector<MediaStreamConstraint*>::iterator it = constraints.begin(); | |
| 157 it != constraints.end(); ++it) { | |
| 158 base::DictionaryValue* constraint = &(*it)->mandatory.additional_properties; | |
| 159 constraint->SetString(kMediaStreamSource, kMediaStreamSourceTab); | |
| 160 constraint->SetString(kMediaStreamSourceId, device_id); | |
| 161 } | |
| 162 | |
| 163 TabCaptureRegistry* registry = TabCaptureRegistry::Get(GetProfile()); | 236 TabCaptureRegistry* registry = TabCaptureRegistry::Get(GetProfile()); |
| 164 if (!registry->AddRequest(target_contents, extension_id)) { | 237 if (!registry->AddRequest(target_contents, extension_id, false)) { |
| 165 error_ = kCapturingSameTab; | 238 error_ = kCapturingSameTab; |
| 166 return false; | 239 return false; |
| 167 } | 240 } |
| 241 AddMediaStreamSourceOptions(¶ms->options, target_contents); | |
| 168 | 242 |
| 169 // Copy the result from our modified input parameters. This will be | 243 // At this point, everything is set up in the browser process. It's now up to |
| 170 // intercepted by custom bindings which will build and send the special | 244 // the custom JS bindings in the renderer to request a MediaStream using |
| 171 // WebRTC user media request. | 245 // navigator.webkitGetUserMedia(). The result dictionary, passed to |
| 246 // SetResult() here, contains the extra "hidden options" that will allow the | |
| 247 // Chrome platform implementation for getUserMedia() to start the virtual | |
| 248 // audio/video capture devices and set up all the data flows. The custom JS | |
| 249 // bindings can be found here: | |
| 250 // chrome/renderer/resources/extensions/tab_capture_custom_bindings.js | |
| 172 base::DictionaryValue* result = new base::DictionaryValue(); | 251 base::DictionaryValue* result = new base::DictionaryValue(); |
| 173 result->MergeDictionary(params->options.ToValue().get()); | 252 result->MergeDictionary(params->options.ToValue().get()); |
| 174 | |
| 175 SetResult(result); | 253 SetResult(result); |
| 176 return true; | 254 return true; |
| 177 } | 255 } |
| 178 | 256 |
| 179 bool TabCaptureGetCapturedTabsFunction::RunSync() { | 257 bool TabCaptureGetCapturedTabsFunction::RunSync() { |
| 180 TabCaptureRegistry* registry = TabCaptureRegistry::Get(GetProfile()); | 258 TabCaptureRegistry* registry = TabCaptureRegistry::Get(GetProfile()); |
| 181 base::ListValue* const list = new base::ListValue(); | 259 base::ListValue* const list = new base::ListValue(); |
| 182 if (registry) | 260 if (registry) |
| 183 registry->GetCapturedTabs(extension()->id(), list); | 261 registry->GetCapturedTabs(extension()->id(), list); |
| 184 SetResult(list); | 262 SetResult(list); |
| 185 return true; | 263 return true; |
| 186 } | 264 } |
| 187 | 265 |
| 266 bool TabCaptureCapturePresentationFunction::RunSync() { | |
| 267 scoped_ptr<TabCapture::CapturePresentation::Params> params = | |
| 268 TabCapture::CapturePresentation::Params::Create(*args_); | |
| 269 EXTENSION_FUNCTION_VALIDATE(params.get()); | |
| 270 | |
| 271 // One or both of two possible switches will enable this API for use. | |
| 272 if (!base::CommandLine::ForCurrentProcess()->HasSwitch( | |
| 273 ::switches::kEnableCapturePresentationApi) && | |
| 274 !::switches::MediaRouterEnabled()) { | |
| 275 SetError(kCapturePresentationApiNotEnabled); | |
| 276 return false; | |
| 277 } | |
| 278 | |
| 279 if (!extension()->permissions_data()->HasAPIPermission( | |
| 280 APIPermission::kTabCapture) && | |
| 281 base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( | |
| 282 switches::kWhitelistedExtensionID) != extension()->id() && | |
| 283 !SimpleFeature::IsIdInArray(extension()->id(), kChromecastExtensionIds, | |
| 284 arraysize(kChromecastExtensionIds))) { | |
| 285 error_ = kMissingTabCapturePermission; | |
| 286 return false; | |
| 287 } | |
| 288 | |
| 289 const GURL start_url(params->presentation_url); | |
| 290 if (!IsAcceptablePresentationUrl(start_url)) { | |
| 291 SetError(kInvalidPresentationUrl); | |
| 292 return false; | |
| 293 } | |
| 294 | |
| 295 if (!OptionsSpecifyAudioOrVideo(params->options)) { | |
| 296 SetError(kNoAudioOrVideo); | |
| 297 return false; | |
| 298 } | |
| 299 | |
| 300 content::WebContents* const extension_web_contents = GetSenderWebContents(); | |
| 301 if (!extension_web_contents) { | |
| 302 SetError(kMissingExtensionPage); | |
| 303 return false; | |
| 304 } | |
| 305 | |
| 306 OffscreenPresentation* const presentation = | |
| 307 OffscreenPresentationsOwner::Get(extension_web_contents) | |
| 308 ->FindOrStartPresentation(start_url, | |
| 309 params->presentation_id, | |
| 310 DeterminePresentationSize(params->options)); | |
| 311 if (!presentation) { | |
| 312 SetError(kTooManyPresentations); | |
| 313 return false; | |
| 314 } | |
| 315 if (!TabCaptureRegistry::Get(browser_context())->AddRequest( | |
| 316 presentation->web_contents(), extension()->id(), true)) { | |
| 317 SetError(kCapturingSamePresentation); | |
| 318 return false; | |
| 319 } | |
| 320 AddMediaStreamSourceOptions(¶ms->options, presentation->web_contents()); | |
| 321 | |
| 322 // At this point, everything is set up in the browser process. It's now up to | |
| 323 // the custom JS bindings in the renderer to complete the request. See the | |
| 324 // comment at end of TabCaptureCaptureFunction::RunSync() for more details. | |
| 325 base::DictionaryValue* const result = new base::DictionaryValue(); | |
| 326 result->MergeDictionary(params->options.ToValue().get()); | |
| 327 SetResult(result); | |
| 328 return true; | |
| 329 } | |
| 330 | |
| 188 } // namespace extensions | 331 } // namespace extensions |
| OLD | NEW |