|  | // Copyright 2018 The Chromium Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "headless/lib/browser/protocol/headless_handler.h" | 
|  |  | 
|  | #include "base/base64.h" | 
|  | #include "base/base_switches.h" | 
|  | #include "base/command_line.h" | 
|  | #include "base/lazy_instance.h" | 
|  | #include "cc/base/switches.h" | 
|  | #include "components/viz/common/frame_sinks/begin_frame_args.h" | 
|  | #include "components/viz/common/switches.h" | 
|  | #include "content/public/common/content_switches.h" | 
|  | #include "headless/lib/browser/headless_browser_impl.h" | 
|  | #include "headless/lib/browser/headless_web_contents_impl.h" | 
|  | #include "third_party/skia/include/core/SkBitmap.h" | 
|  | #include "ui/base/resource/resource_bundle.h" | 
|  | #include "ui/gfx/codec/jpeg_codec.h" | 
|  | #include "ui/gfx/codec/png_codec.h" | 
|  | #include "ui/gfx/image/image.h" | 
|  | #include "ui/gfx/image/image_util.h" | 
|  |  | 
|  | namespace headless { | 
|  | namespace protocol { | 
|  |  | 
|  | using HeadlessExperimental::ScreenshotParams; | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | base::LazyInstance<std::set<HeadlessHandler*>>::Leaky g_instances = | 
|  | LAZY_INSTANCE_INITIALIZER; | 
|  |  | 
|  | enum class ImageEncoding { kPng, kJpeg }; | 
|  | constexpr int kDefaultScreenshotQuality = 80; | 
|  |  | 
|  | std::string EncodeBitmap(const SkBitmap& bitmap, | 
|  | ImageEncoding encoding, | 
|  | int quality) { | 
|  | gfx::Image image = gfx::Image::CreateFrom1xBitmap(bitmap); | 
|  | DCHECK(!image.IsEmpty()); | 
|  |  | 
|  | scoped_refptr<base::RefCountedMemory> data; | 
|  | if (encoding == ImageEncoding::kPng) { | 
|  | data = image.As1xPNGBytes(); | 
|  | } else if (encoding == ImageEncoding::kJpeg) { | 
|  | scoped_refptr<base::RefCountedBytes> bytes(new base::RefCountedBytes()); | 
|  | if (gfx::JPEG1xEncodedDataFromImage(image, quality, &bytes->data())) | 
|  | data = bytes; | 
|  | } | 
|  |  | 
|  | if (!data || !data->front()) | 
|  | return std::string(); | 
|  |  | 
|  | std::string base_64_data; | 
|  | base::Base64Encode( | 
|  | base::StringPiece(reinterpret_cast<const char*>(data->front()), | 
|  | data->size()), | 
|  | &base_64_data); | 
|  | return base_64_data; | 
|  | } | 
|  |  | 
|  | void OnBeginFrameFinished( | 
|  | std::unique_ptr<HeadlessHandler::BeginFrameCallback> callback, | 
|  | ImageEncoding encoding, | 
|  | int quality, | 
|  | bool has_damage, | 
|  | std::unique_ptr<SkBitmap> bitmap) { | 
|  | if (!bitmap || bitmap->drawsNothing()) { | 
|  | callback->sendSuccess(has_damage, Maybe<String>()); | 
|  | return; | 
|  | } | 
|  | std::string data = EncodeBitmap(*bitmap, encoding, quality); | 
|  | callback->sendSuccess(has_damage, std::move(data)); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | // static | 
|  | void HeadlessHandler::OnNeedsBeginFrames( | 
|  | HeadlessWebContentsImpl* headless_contents, | 
|  | bool needs_begin_frames) { | 
|  | if (!g_instances.IsCreated()) | 
|  | return; | 
|  | for (const HeadlessHandler* handler : g_instances.Get()) { | 
|  | if (handler->enabled_ && handler->frontend_) | 
|  | handler->frontend_->NeedsBeginFramesChanged(needs_begin_frames); | 
|  | } | 
|  | } | 
|  |  | 
|  | HeadlessHandler::HeadlessHandler(base::WeakPtr<HeadlessBrowserImpl> browser, | 
|  | content::WebContents* web_contents) | 
|  | : DomainHandler(HeadlessExperimental::Metainfo::domainName, browser), | 
|  | web_contents_(web_contents) {} | 
|  |  | 
|  | HeadlessHandler::~HeadlessHandler() { | 
|  | DCHECK(g_instances.Get().find(this) == g_instances.Get().end()); | 
|  | } | 
|  |  | 
|  | void HeadlessHandler::Wire(UberDispatcher* dispatcher) { | 
|  | frontend_.reset(new HeadlessExperimental::Frontend(dispatcher->channel())); | 
|  | HeadlessExperimental::Dispatcher::wire(dispatcher, this); | 
|  | } | 
|  |  | 
|  | Response HeadlessHandler::Enable() { | 
|  | g_instances.Get().insert(this); | 
|  | HeadlessWebContentsImpl* headless_contents = | 
|  | HeadlessWebContentsImpl::From(browser().get(), web_contents_); | 
|  | enabled_ = true; | 
|  | if (headless_contents->needs_external_begin_frames() && frontend_) | 
|  | frontend_->NeedsBeginFramesChanged(true); | 
|  | return Response::OK(); | 
|  | } | 
|  |  | 
|  | Response HeadlessHandler::Disable() { | 
|  | enabled_ = false; | 
|  | g_instances.Get().erase(this); | 
|  | return Response::OK(); | 
|  | } | 
|  |  | 
|  | void HeadlessHandler::BeginFrame(Maybe<double> in_frame_time_ticks, | 
|  | Maybe<double> in_interval, | 
|  | Maybe<bool> in_no_display_updates, | 
|  | Maybe<ScreenshotParams> screenshot, | 
|  | std::unique_ptr<BeginFrameCallback> callback) { | 
|  | HeadlessWebContentsImpl* headless_contents = | 
|  | HeadlessWebContentsImpl::From(browser().get(), web_contents_); | 
|  | if (!headless_contents->begin_frame_control_enabled()) { | 
|  | callback->sendFailure(Response::Error( | 
|  | "Command is only supported if BeginFrameControl is enabled.")); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (!base::CommandLine::ForCurrentProcess()->HasSwitch( | 
|  | ::switches::kRunAllCompositorStagesBeforeDraw)) { | 
|  | LOG(WARNING) << "BeginFrameControl commands are designed to be used with " | 
|  | "--run-all-compositor-stages-before-draw, see " | 
|  | "https://goo.gl/3zHXhB for more info."; | 
|  | } | 
|  |  | 
|  | base::TimeTicks frame_time_ticks; | 
|  | base::TimeDelta interval; | 
|  | bool no_display_updates = in_no_display_updates.fromMaybe(false); | 
|  |  | 
|  | if (in_frame_time_ticks.isJust()) { | 
|  | frame_time_ticks = base::TimeTicks() + base::TimeDelta::FromMillisecondsD( | 
|  | in_frame_time_ticks.fromJust()); | 
|  | } else { | 
|  | frame_time_ticks = base::TimeTicks::Now(); | 
|  | } | 
|  |  | 
|  | if (in_interval.isJust()) { | 
|  | double interval_double = in_interval.fromJust(); | 
|  | if (interval_double <= 0) { | 
|  | callback->sendFailure( | 
|  | Response::InvalidParams("interval has to be greater than 0")); | 
|  | return; | 
|  | } | 
|  | interval = base::TimeDelta::FromMillisecondsD(interval_double); | 
|  | } else { | 
|  | interval = viz::BeginFrameArgs::DefaultInterval(); | 
|  | } | 
|  |  | 
|  | base::TimeTicks deadline = frame_time_ticks + interval; | 
|  |  | 
|  | bool capture_screenshot = false; | 
|  | ImageEncoding encoding; | 
|  | int quality; | 
|  |  | 
|  | if (screenshot.isJust()) { | 
|  | capture_screenshot = true; | 
|  | const std::string format = | 
|  | screenshot.fromJust()->GetFormat(ScreenshotParams::FormatEnum::Png); | 
|  | if (format != ScreenshotParams::FormatEnum::Png && | 
|  | format != ScreenshotParams::FormatEnum::Jpeg) { | 
|  | callback->sendFailure( | 
|  | Response::InvalidParams("Invalid screenshot.format")); | 
|  | return; | 
|  | } | 
|  | encoding = format == ScreenshotParams::FormatEnum::Png | 
|  | ? ImageEncoding::kPng | 
|  | : ImageEncoding::kJpeg; | 
|  | quality = screenshot.fromJust()->GetQuality(kDefaultScreenshotQuality); | 
|  | if (quality < 0 || quality > 100) { | 
|  | callback->sendFailure(Response::InvalidParams( | 
|  | "screenshot.quality has to be in range 0..100")); | 
|  | return; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (base::CommandLine::ForCurrentProcess()->HasSwitch( | 
|  | ::switches::kRunAllCompositorStagesBeforeDraw) && | 
|  | headless_contents->HasPendingFrame()) { | 
|  | LOG(WARNING) << "A BeginFrame is already in flight. In " | 
|  | "--run-all-compositor-stages-before-draw mode, only a " | 
|  | "single BeginFrame should be active at the same time."; | 
|  | } | 
|  |  | 
|  | headless_contents->BeginFrame( | 
|  | frame_time_ticks, deadline, interval, no_display_updates, | 
|  | capture_screenshot, | 
|  | base::BindOnce(&OnBeginFrameFinished, std::move(callback), encoding, | 
|  | quality)); | 
|  | } | 
|  |  | 
|  | }  // namespace protocol | 
|  | }  // namespace headless |