Chromium Code Reviews| Index: chrome/browser/extensions/api/tab_capture/tab_capture_api.cc |
| diff --git a/chrome/browser/extensions/api/tab_capture/tab_capture_api.cc b/chrome/browser/extensions/api/tab_capture/tab_capture_api.cc |
| index 57605300a2b609207d9a78f950a03facd1e15ed9..ff336c9cdc3826d3844b1dc46d68ac147d45dcc0 100644 |
| --- a/chrome/browser/extensions/api/tab_capture/tab_capture_api.cc |
| +++ b/chrome/browser/extensions/api/tab_capture/tab_capture_api.cc |
| @@ -6,6 +6,7 @@ |
| #include "chrome/browser/extensions/api/tab_capture/tab_capture_api.h" |
| +#include <algorithm> |
| #include <set> |
| #include <string> |
| #include <vector> |
| @@ -13,6 +14,7 @@ |
| #include "base/command_line.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/values.h" |
| +#include "chrome/browser/extensions/api/tab_capture/offscreen_presentation.h" |
| #include "chrome/browser/extensions/api/tab_capture/tab_capture_registry.h" |
| #include "chrome/browser/extensions/extension_renderer_state.h" |
| #include "chrome/browser/profiles/profile.h" |
| @@ -20,8 +22,10 @@ |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| +#include "chrome/common/chrome_switches.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| +#include "content/public/browser/web_contents.h" |
| #include "extensions/common/features/feature.h" |
| #include "extensions/common/features/feature_provider.h" |
| #include "extensions/common/features/simple_feature.h" |
| @@ -43,6 +47,18 @@ const char kGrantError[] = |
| "Extension has not been invoked for the current page (see activeTab " |
| "permission). Chrome pages cannot be captured."; |
| +const char kNotWhitelistedForOffscreenTabApi[] = |
| + "Extension is not whitelisted for use of the unstable, in-development " |
| + "chrome.tabCapture.captureOffscreenTab API."; |
| +const char kInvalidStartUrl[] = |
| + "Invalid/Missing/Malformatted starting URL for off-screen tab."; |
| +const char kMissingExtensionPage[] = |
| + "tabCapture API was not invoked from an extension background page."; |
| +const char kTooManyOffscreenTabs[] = |
| + "Extension has already started too many off-screen tabs."; |
| +const char kCapturingSameOffscreenTab[] = |
| + "Cannot capture the same off-screen tab more than once."; |
| + |
| // Keys/values for media stream constraints. |
| const char kMediaStreamSource[] = "chromeMediaSource"; |
| const char kMediaStreamSourceId[] = "chromeMediaSourceId"; |
| @@ -52,6 +68,70 @@ const char kMediaStreamSourceTab[] = "tab"; |
| // throttling mode in the capture pipeline. |
| const char kEnableAutoThrottlingKey[] = "enableAutoThrottling"; |
| +bool OptionsSpecifyAudioOrVideo(const TabCapture::CaptureOptions& options) { |
| + return (options.audio && *options.audio) || (options.video && *options.video); |
| +} |
| + |
| +bool IsAcceptableOffscreenTabUrl(const GURL& url) { |
| + return url.is_valid() && (url.SchemeIsHTTPOrHTTPS() || url.SchemeIs("data")); |
| +} |
| + |
| +// Add Chrome-specific source identifiers to the MediaStreamConstraints objects |
| +// in |options| to provide references to the |target_contents| to be captured. |
| +void AddMediaStreamSourceConstraints(content::WebContents* target_contents, |
| + TabCapture::CaptureOptions* options) { |
| + DCHECK(options); |
| + DCHECK(target_contents); |
| + |
| + MediaStreamConstraint* constraints_to_modify[2] = { nullptr, nullptr }; |
| + |
| + if (options->audio && *options->audio) { |
| + if (!options->audio_constraints) |
| + options->audio_constraints.reset(new MediaStreamConstraint); |
| + constraints_to_modify[0] = options->audio_constraints.get(); |
| + } |
| + |
| + bool enable_auto_throttling = false; |
| + if (options->video && *options->video) { |
| + if (options->video_constraints) { |
| + // Check for the Tab Capture-specific video constraint for enabling |
| + // automatic resolution/rate throttling mode in the capture pipeline. See |
| + // implementation comments for content::WebContentsVideoCaptureDevice. |
| + base::DictionaryValue& props = |
| + options->video_constraints->mandatory.additional_properties; |
| + if (!props.GetBooleanWithoutPathExpansion( |
| + kEnableAutoThrottlingKey, &enable_auto_throttling)) { |
| + enable_auto_throttling = false; |
| + } |
| + // Remove the key from the properties to avoid an "unrecognized |
| + // constraint" error in the renderer. |
| + props.RemoveWithoutPathExpansion(kEnableAutoThrottlingKey, nullptr); |
| + } else { |
| + options->video_constraints.reset(new MediaStreamConstraint); |
| + } |
| + constraints_to_modify[1] = options->video_constraints.get(); |
| + } |
| + |
| + // Format the device ID that references the target tab. |
| + content::RenderFrameHost* const main_frame = target_contents->GetMainFrame(); |
| + // TODO(miu): We should instead use a "randomly generated device ID" scheme, |
| + // like that employed by the desktop capture API. http://crbug.com/163100 |
| + const std::string device_id = base::StringPrintf( |
| + "web-contents-media-stream://%i:%i%s", |
| + main_frame->GetProcess()->GetID(), |
| + main_frame->GetRoutingID(), |
| + enable_auto_throttling ? "?throttling=auto" : ""); |
| + |
| + // Append chrome specific tab constraints. |
| + for (MediaStreamConstraint* msc : constraints_to_modify) { |
| + if (!msc) |
| + continue; |
| + base::DictionaryValue* constraint = &msc->mandatory.additional_properties; |
| + constraint->SetString(kMediaStreamSource, kMediaStreamSourceTab); |
| + constraint->SetString(kMediaStreamSourceId, device_id); |
| + } |
| +} |
| + |
| } // namespace |
| // Whitelisted extensions that do not check for a browser action grant because |
| @@ -78,7 +158,7 @@ const char* const kMediaRouterExtensionIds[] = { |
| bool TabCaptureCaptureFunction::RunSync() { |
| scoped_ptr<api::tab_capture::Capture::Params> params = |
| TabCapture::Capture::Params::Create(*args_); |
| - EXTENSION_FUNCTION_VALIDATE(params.get()); |
| + EXTENSION_FUNCTION_VALIDATE(params); |
| // Figure out the active WebContents and retrieve the needed ids. |
| Browser* target_browser = chrome::FindAnyBrowser( |
| @@ -112,76 +192,30 @@ bool TabCaptureCaptureFunction::RunSync() { |
| return false; |
| } |
| - // Create a constraints vector. We will modify all the constraints in this |
| - // vector to append our chrome specific constraints. |
| - std::vector<MediaStreamConstraint*> constraints; |
| - bool has_audio = params->options.audio.get() && *params->options.audio.get(); |
| - bool has_video = params->options.video.get() && *params->options.video.get(); |
| - |
| - if (!has_audio && !has_video) { |
| + if (!OptionsSpecifyAudioOrVideo(params->options)) { |
| error_ = kNoAudioOrVideo; |
| return false; |
| } |
| - if (has_audio) { |
| - if (!params->options.audio_constraints.get()) |
| - params->options.audio_constraints.reset(new MediaStreamConstraint); |
| - |
| - constraints.push_back(params->options.audio_constraints.get()); |
| - } |
| - |
| - bool enable_auto_throttling = false; |
| - if (has_video) { |
| - if (params->options.video_constraints.get()) { |
| - // Check for the Tab Capture-specific video constraint for enabling |
| - // automatic resolution/rate throttling mode in the capture pipeline. See |
| - // implementation comments for content::WebContentsVideoCaptureDevice. |
| - base::DictionaryValue& props = |
| - params->options.video_constraints->mandatory.additional_properties; |
| - if (!props.GetBooleanWithoutPathExpansion( |
| - kEnableAutoThrottlingKey, &enable_auto_throttling)) { |
| - enable_auto_throttling = false; |
| - } |
| - // Remove the key from the properties to avoid an "unrecognized |
| - // constraint" error in the renderer. |
| - props.RemoveWithoutPathExpansion(kEnableAutoThrottlingKey, nullptr); |
| - } else { |
| - params->options.video_constraints.reset(new MediaStreamConstraint); |
| - } |
| - |
| - constraints.push_back(params->options.video_constraints.get()); |
| - } |
| - |
| - // Device id we use for Tab Capture. |
| - content::RenderFrameHost* const main_frame = target_contents->GetMainFrame(); |
| - // TODO(miu): We should instead use a "randomly generated device ID" scheme, |
| - // like that employed by the desktop capture API. http://crbug.com/163100 |
| - const std::string device_id = base::StringPrintf( |
| - "web-contents-media-stream://%i:%i%s", |
| - main_frame->GetProcess()->GetID(), |
| - main_frame->GetRoutingID(), |
| - enable_auto_throttling ? "?throttling=auto" : ""); |
| - |
| - // Append chrome specific tab constraints. |
| - for (std::vector<MediaStreamConstraint*>::iterator it = constraints.begin(); |
| - it != constraints.end(); ++it) { |
| - base::DictionaryValue* constraint = &(*it)->mandatory.additional_properties; |
| - constraint->SetString(kMediaStreamSource, kMediaStreamSourceTab); |
| - constraint->SetString(kMediaStreamSourceId, device_id); |
| - } |
| - |
| TabCaptureRegistry* registry = TabCaptureRegistry::Get(GetProfile()); |
| - if (!registry->AddRequest(target_contents, extension_id)) { |
| + if (!registry->AddRequest(target_contents, extension_id, false)) { |
| + // TODO(miu): Allow multiple consumers of single tab capture. |
| + // http://crbug.com/535336 |
| error_ = kCapturingSameTab; |
| return false; |
| } |
| + AddMediaStreamSourceConstraints(target_contents, ¶ms->options); |
| - // Copy the result from our modified input parameters. This will be |
| - // intercepted by custom bindings which will build and send the special |
| - // WebRTC user media request. |
| + // At this point, everything is set up in the browser process. It's now up to |
| + // the custom JS bindings in the extension's render process to request a |
| + // MediaStream using navigator.webkitGetUserMedia(). The result dictionary, |
| + // passed to SetResult() here, contains the extra "hidden options" that will |
| + // allow the Chrome platform implementation for getUserMedia() to start the |
| + // virtual audio/video capture devices and set up all the data flows. The |
| + // custom JS bindings can be found here: |
| + // chrome/renderer/resources/extensions/tab_capture_custom_bindings.js |
| base::DictionaryValue* result = new base::DictionaryValue(); |
| result->MergeDictionary(params->options.ToValue().get()); |
| - |
| SetResult(result); |
| return true; |
| } |
| @@ -195,4 +229,121 @@ bool TabCaptureGetCapturedTabsFunction::RunSync() { |
| return true; |
| } |
| +bool TabCaptureCaptureOffscreenTabFunction::RunSync() { |
| + scoped_ptr<TabCapture::CaptureOffscreenTab::Params> params = |
| + TabCapture::CaptureOffscreenTab::Params::Create(*args_); |
| + EXTENSION_FUNCTION_VALIDATE(params); |
| + |
| + // Make sure the extension is whitelisted for using this API, or this is a |
| + // Canary/Dev-channel build of Chrome. |
| + const bool is_whitelisted_extension = |
|
not at google - send to devlin
2015/09/30 01:32:11
A better way to whitelist is by adding a rule for
miu
2015/09/30 19:46:38
Added a TODO for this. The reason is that there i
|
| + base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| + switches::kWhitelistedExtensionID) == extension()->id() || |
| + SimpleFeature::IsIdInArray(extension()->id(), kChromecastExtensionIds, |
| + arraysize(kChromecastExtensionIds)) || |
| + SimpleFeature::IsIdInArray(extension()->id(), kMediaRouterExtensionIds, |
| + arraysize(kMediaRouterExtensionIds)); |
| + if (!is_whitelisted_extension) { |
| + error_ = kNotWhitelistedForOffscreenTabApi; |
| + return false; |
| + } |
| + |
| + const GURL start_url(params->start_url); |
| + if (!IsAcceptableOffscreenTabUrl(start_url)) { |
| + SetError(kInvalidStartUrl); |
| + return false; |
| + } |
| + |
| + if (!OptionsSpecifyAudioOrVideo(params->options)) { |
| + SetError(kNoAudioOrVideo); |
| + return false; |
| + } |
| + |
| + content::WebContents* const extension_web_contents = GetSenderWebContents(); |
| + if (!extension_web_contents) { |
| + SetError(kMissingExtensionPage); |
| + return false; |
| + } |
| + |
| + OffscreenPresentation* const offscreen_tab = |
| + OffscreenPresentationsOwner::Get(extension_web_contents) |
| + ->StartPresentation( |
| + start_url, |
| + (is_whitelisted_extension && params->options.presentation_id) ? |
| + *params->options.presentation_id : std::string(), |
| + DetermineInitialSize(params->options)); |
| + if (!offscreen_tab) { |
| + SetError(kTooManyOffscreenTabs); |
| + return false; |
| + } |
| + |
| + if (!TabCaptureRegistry::Get(browser_context())->AddRequest( |
| + offscreen_tab->web_contents(), extension()->id(), true)) { |
| + // TODO(miu): Allow multiple consumers of single tab capture. |
| + // http://crbug.com/535336 |
| + SetError(kCapturingSameOffscreenTab); |
| + return false; |
| + } |
| + AddMediaStreamSourceConstraints(offscreen_tab->web_contents(), |
| + ¶ms->options); |
| + |
| + // At this point, everything is set up in the browser process. It's now up to |
| + // the custom JS bindings in the extension's render process to complete the |
| + // request. See the comment at end of TabCaptureCaptureFunction::RunSync() |
| + // for more details. |
| + base::DictionaryValue* const result = new base::DictionaryValue(); |
| + result->MergeDictionary(params->options.ToValue().get()); |
| + SetResult(result); |
| + return true; |
| +} |
| + |
| +// static |
| +gfx::Size TabCaptureCaptureOffscreenTabFunction::DetermineInitialSize( |
| + const TabCapture::CaptureOptions& options) { |
| + static const int kDefaultWidth = 1280; |
| + static const int kDefaultHeight = 720; |
| + |
| + if (!options.video_constraints) |
| + return gfx::Size(kDefaultWidth, kDefaultHeight); |
| + |
| + gfx::Size min_size; |
| + int width = -1; |
| + int height = -1; |
| + const base::DictionaryValue& mandatory_properties = |
| + options.video_constraints->mandatory.additional_properties; |
| + if (mandatory_properties.GetInteger("maxWidth", &width) && width >= 0 && |
| + mandatory_properties.GetInteger("maxHeight", &height) && height >= 0) { |
| + return gfx::Size(width, height); |
| + } |
| + if (mandatory_properties.GetInteger("minWidth", &width) && width >= 0 && |
| + mandatory_properties.GetInteger("minHeight", &height) && height >= 0) { |
| + min_size.SetSize(width, height); |
| + } |
| + |
| + // Use optional size constraints if no mandatory ones were provided. |
| + if (options.video_constraints->optional) { |
| + const base::DictionaryValue& optional_properties = |
| + options.video_constraints->optional->additional_properties; |
| + if (optional_properties.GetInteger("maxWidth", &width) && width >= 0 && |
| + optional_properties.GetInteger("maxHeight", &height) && height >= 0) { |
| + if (min_size.IsEmpty()) { |
| + return gfx::Size(width, height); |
| + } else { |
| + return gfx::Size(std::max(width, min_size.width()), |
| + std::max(height, min_size.height())); |
| + } |
| + } |
| + if (min_size.IsEmpty() && |
| + optional_properties.GetInteger("minWidth", &width) && width >= 0 && |
| + optional_properties.GetInteger("minHeight", &height) && height >= 0) { |
| + min_size.SetSize(width, height); |
| + } |
| + } |
| + |
| + // No maximum size was provided, so just return the default size bounded by |
| + // the minimum size. |
| + return gfx::Size(std::max(kDefaultWidth, min_size.width()), |
| + std::max(kDefaultHeight, min_size.height())); |
| +} |
| + |
| } // namespace extensions |