chromium/chrome/browser/ui/ash/capture_mode/recording_service_browsertest.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <memory>
#include <string>

#include "ash/capture_mode/capture_mode_types.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/capture_mode/capture_mode_test_api.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "base/check.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/gtest_tags.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/download/download_prefs.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/capture_mode/chrome_capture_mode_delegate.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/ash/components/dbus/cros_disks/cros_disks_client.h"
#include "content/public/test/browser_test.h"
#include "media/base/media_tracks.h"
#include "media/base/mock_media_log.h"
#include "media/base/stream_parser.h"
#include "media/formats/webm/webm_stream_parser.h"
#include "third_party/skia/include/codec/SkGifDecoder.h"
#include "third_party/skia/include/core/SkImage.h"
#include "ui/aura/window.h"
#include "ui/display/screen.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/point.h"

namespace {

// Runs a loop for the given |milliseconds| duration.
void WaitForMilliseconds(int milliseconds) {
#if defined(MEMORY_SANITIZER)
  // MSAN runs much slower than regular tests, so give it more time to complete
  milliseconds *= 2;
#endif

  base::RunLoop loop;
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE, loop.QuitClosure(), base::Milliseconds(milliseconds));
  loop.Run();
}

// Wait for the service to flush all the video chunks to ash, and waits for the
// file contents to be fully saved, and returns the path where the video file
// was saved.
base::FilePath WaitForVideoFileToBeSaved() {
  base::FilePath result;
  base::RunLoop run_loop;
  ash::CaptureModeTestApi().SetOnCaptureFileSavedCallback(
      base::BindLambdaForTesting([&](const base::FilePath& path) {
        result = path;
        run_loop.Quit();
      }));
  run_loop.Run();
  return result;
}

// Attempts to load and decode the GIF image whose file is at the given `path`,
// and verifies that the decoding is successful.
void VerifyGifImage(const base::FilePath& path) {
  ASSERT_TRUE(path.MatchesExtension(".gif"));

  base::ScopedAllowBlockingForTesting allow_blocking;
  ASSERT_TRUE(base::PathExists(path));
  std::string file_content;
  EXPECT_TRUE(base::ReadFileToString(path, &file_content));
  EXPECT_FALSE(file_content.empty());
  sk_sp<SkData> data =
      SkData::MakeWithoutCopy(file_content.data(), file_content.size());
  ASSERT_TRUE(SkGifDecoder::IsGif(data->bytes(), data->size()));
  std::unique_ptr<SkCodec> codec = SkGifDecoder::Decode(data, nullptr);
  ASSERT_TRUE(codec);
  SkImageInfo targetInfo = codec->getInfo().makeAlphaType(kPremul_SkAlphaType);
  sk_sp<SkImage> image = std::get<0>(codec->getImage(targetInfo));
  ASSERT_TRUE(image);
}

// Verifies the contents of a WebM file by parsing it.
class WebmVerifier {
 public:
  WebmVerifier() {
    webm_parser_.Init(
        base::BindOnce(&WebmVerifier::OnInit, base::Unretained(this)),
        base::BindRepeating(&WebmVerifier::OnNewConfig, base::Unretained(this)),
        base::BindRepeating(&WebmVerifier::OnNewBuffers,
                            base::Unretained(this)),
        base::BindRepeating(&WebmVerifier::OnEncryptedMediaInitData,
                            base::Unretained(this)),
        base::BindRepeating(&WebmVerifier::OnNewMediaSegment,
                            base::Unretained(this)),
        base::BindRepeating(&WebmVerifier::OnEndMediaSegment,
                            base::Unretained(this)),
        &media_log_);
  }
  WebmVerifier(const WebmVerifier&) = delete;
  WebmVerifier& operator=(const WebmVerifier&) = delete;
  ~WebmVerifier() = default;

  // Parses the given |webm_file_content| and returns true on success.
  bool Verify(const std::string& webm_file_content) {
    if (!webm_parser_.AppendToParseBuffer(
            base::as_byte_span(webm_file_content))) {
      return false;
    }

    // Run the segment parser loop one time with the full size of the appended
    // data to ensure the parser has had a chance to parse all the appended
    // bytes.
    media::StreamParser::ParseStatus result =
        webm_parser_.Parse(webm_file_content.size());

    // Note that media::StreamParser::ParseStatus::kSuccessHasMoreData is deemed
    // a verification failure here, since the parser was told to parse all the
    // appended bytes and should not have uninspected data afterwards.
    return result == media::StreamParser::ParseStatus::kSuccess;
  }

 private:
  void OnInit(const media::StreamParser::InitParameters&) {}
  bool OnNewConfig(std::unique_ptr<media::MediaTracks> tracks) { return true; }
  bool OnNewBuffers(const media::StreamParser::BufferQueueMap& map) {
    return true;
  }
  void OnEncryptedMediaInitData(media::EmeInitDataType,
                                const std::vector<uint8_t>&) {}
  void OnNewMediaSegment() {}
  void OnEndMediaSegment() {}

  media::WebMStreamParser webm_parser_;
  media::MockMediaLog media_log_;
};

}  // namespace

class RecordingServiceBrowserTest : public InProcessBrowserTest {
 public:
  RecordingServiceBrowserTest() = default;
  RecordingServiceBrowserTest(const RecordingServiceBrowserTest&) = delete;
  RecordingServiceBrowserTest& operator=(const RecordingServiceBrowserTest&) =
      delete;
  ~RecordingServiceBrowserTest() override = default;

  // InProcessBrowserTest:
  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    aura::Window* browser_window = GetBrowserWindow();
    event_generator_ = std::make_unique<ui::test::EventGenerator>(
        browser_window->GetRootWindow(), browser_window);
    // To improve the test efficiency, we set the display to a small size.
    display::test::DisplayManagerTestApi(ash::ShellTestApi().display_manager())
        .UpdateDisplay("300x200");
    // To avoid flaky tests, we disable audio recording, since the bots won't
    // capture any audio and won't produce any audio frames. This will cause the
    // muxer to discard video frames if it expects audio frames but got none,
    // which may cause the produced webm file to be empty. See issues
    // https://crbug.com/1151167 and https://crbug.com/1151418.
    ash::CaptureModeTestApi().SetAudioRecordingMode(
        ash::AudioRecordingMode::kOff);
  }

  aura::Window* GetBrowserWindow() const {
    return browser()->window()->GetNativeWindow();
  }

  ui::test::EventGenerator* GetEventGenerator() {
    return event_generator_.get();
  }

  // Reads the video file at the given |path| and verifies its WebM contents. At
  // the end it deletes the file to save space, since video files can be big.
  // |allow_empty| can be set to true if an empty video file is possible.
  void VerifyVideoFileAndDelete(const base::FilePath& path,
                                bool allow_empty = false) const {
    base::ScopedAllowBlockingForTesting allow_blocking;
    ASSERT_TRUE(base::PathExists(path));
    std::string file_content;
    EXPECT_TRUE(base::ReadFileToString(path, &file_content));

    if (allow_empty && file_content.empty())
      return;

    EXPECT_FALSE(file_content.empty());
    EXPECT_TRUE(WebmVerifier().Verify(file_content));
    EXPECT_TRUE(base::DeleteFile(path));
  }

  void FinishVideoRecordingTest(ash::CaptureModeTestApi* test_api) {
    test_api->PerformCapture();
    test_api->FlushRecordingServiceForTesting();
    // Record a 1.5-second long video to give it enough time to produce and send
    // video frames in order to exercise all the code paths of the service and
    // its client.
    WaitForMilliseconds(1500);
    test_api->StopVideoRecording();
    const base::FilePath video_path = WaitForVideoFileToBeSaved();
    VerifyVideoFileAndDelete(video_path);
  }

 private:
  std::unique_ptr<ui::test::EventGenerator> event_generator_;
};

// TODO(b/252343017): Flaky on MSAN bots.
#if defined(MEMORY_SANITIZER)
#define MAYBE_RecordFullscreen DISABLED_RecordFullscreen
#else
#define MAYBE_RecordFullscreen RecordFullscreen
#endif
IN_PROC_BROWSER_TEST_F(RecordingServiceBrowserTest, MAYBE_RecordFullscreen) {
  ash::CaptureModeTestApi test_api;
  test_api.StartForFullscreen(/*for_video=*/true);
  FinishVideoRecordingTest(&test_api);
}

IN_PROC_BROWSER_TEST_F(RecordingServiceBrowserTest, RecordWindow) {
  ash::CaptureModeTestApi test_api;
  test_api.StartForWindow(/*for_video=*/true);
  auto* generator = GetEventGenerator();
  // Move the mouse cursor above the browser window to select it for window
  // capture (make sure it doesn't hover over the capture bar).
  generator->MoveMouseTo(GetBrowserWindow()->GetBoundsInScreen().top_center());
  FinishVideoRecordingTest(&test_api);
}

IN_PROC_BROWSER_TEST_F(RecordingServiceBrowserTest, RecordWindowMultiDisplay) {
  display::test::DisplayManagerTestApi(ash::ShellTestApi().display_manager())
      .UpdateDisplay("300x200,301+0-400x350");

  ash::CaptureModeTestApi capture_mode_test_api;
  capture_mode_test_api.StartForWindow(/*for_video=*/true);
  auto* generator = GetEventGenerator();
  // Move the mouse cursor above the browser window to select it for window
  // capture (make sure it doesn't hover over the capture bar).
  generator->MoveMouseTo(GetBrowserWindow()->GetBoundsInScreen().top_center());
  capture_mode_test_api.PerformCapture();
  capture_mode_test_api.FlushRecordingServiceForTesting();

  // Moves the browser window to the display at the given |screen_point|.
  auto move_browser_to_display_at_point = [&](const gfx::Point& screen_point) {
    auto* screen = display::Screen::GetScreen();
    aura::Window* new_root =
        screen->GetWindowAtScreenPoint(screen_point)->GetRootWindow();
    auto* browser_window = GetBrowserWindow();
    EXPECT_NE(new_root, browser_window->GetRootWindow());
    auto* target_container =
        new_root->GetChildById(browser_window->parent()->GetId());
    DCHECK(target_container);
    target_container->AddChild(browser_window);
    EXPECT_EQ(new_root, browser_window->GetRootWindow());
  };

  // Record for a little bit, then move the window to the secondary display.
  WaitForMilliseconds(600);
  move_browser_to_display_at_point(gfx::Point(320, 50));
  capture_mode_test_api.FlushRecordingServiceForTesting();

  // Record for a little bit, then resize the browser window.
  WaitForMilliseconds(600);
  GetBrowserWindow()->SetBounds(gfx::Rect(310, 10, 300, 300));
  capture_mode_test_api.FlushRecordingServiceForTesting();

  // Record for a little bit, then move the browser window back to the smaller
  // display.
  WaitForMilliseconds(600);
  move_browser_to_display_at_point(gfx::Point(0, 0));
  capture_mode_test_api.FlushRecordingServiceForTesting();

  // Record for a little bit, then end recording. The output video file should
  // still be valid.
  WaitForMilliseconds(600);
  capture_mode_test_api.StopVideoRecording();
  const base::FilePath video_path = WaitForVideoFileToBeSaved();
  VerifyVideoFileAndDelete(video_path);
}

// TODO(b/273521375): Flaky.
IN_PROC_BROWSER_TEST_F(RecordingServiceBrowserTest, DISABLED_RecordRegion) {
  ash::CaptureModeTestApi test_api;
  test_api.StartForRegion(/*for_video=*/true);
  // Select a random partial region of the screen.
  test_api.SetUserSelectedRegion(gfx::Rect(10, 20, 100, 50));
  FinishVideoRecordingTest(&test_api);
}

// TODO(b/273521375): Flaky.
IN_PROC_BROWSER_TEST_F(RecordingServiceBrowserTest,
                       DISABLED_RecordingServiceEndpointDropped) {
  ash::CaptureModeTestApi test_api;
  test_api.StartForFullscreen(/*for_video=*/true);
  test_api.PerformCapture();
  test_api.FlushRecordingServiceForTesting();
  WaitForMilliseconds(1000);
  test_api.ResetRecordingServiceRemote();
  const base::FilePath video_path = WaitForVideoFileToBeSaved();
  VerifyVideoFileAndDelete(video_path);
}

IN_PROC_BROWSER_TEST_F(RecordingServiceBrowserTest,
                       RecordingServiceClientEndpointDropped) {
  ash::CaptureModeTestApi test_api;
  test_api.StartForFullscreen(/*for_video=*/true);
  test_api.PerformCapture();
  test_api.FlushRecordingServiceForTesting();
  WaitForMilliseconds(1000);
  test_api.ResetRecordingServiceClientReceiver();
  const base::FilePath video_path = WaitForVideoFileToBeSaved();
  // Due to buffering on the service side, the channel might get dropped before
  // any flushing of those beffers ever happens, and since dropping the client
  // end point will immediately terminate the service, nothing may ever get
  // flushed, and the resulting video file can be empty.
  VerifyVideoFileAndDelete(video_path, /*allow_empty=*/true);
}

// TODO(b/273521375): Flaky.
IN_PROC_BROWSER_TEST_F(RecordingServiceBrowserTest,
                       DISABLED_SuccessiveRecording) {
  ash::CaptureModeTestApi test_api;
  // Do a fullscreen recording, followed by a region recording.
  test_api.StartForFullscreen(/*for_video=*/true);
  FinishVideoRecordingTest(&test_api);
  test_api.StartForRegion(/*for_video=*/true);
  test_api.SetUserSelectedRegion(gfx::Rect(50, 200));
  FinishVideoRecordingTest(&test_api);
}

// Tests that recording will be interrupted once screen capture becomes locked.
// TODO(b/273521375): Flaky.
IN_PROC_BROWSER_TEST_F(RecordingServiceBrowserTest,
                       DISABLED_RecordingInterruptedOnCaptureLocked) {
  ash::CaptureModeTestApi test_api;
  test_api.StartForFullscreen(/*for_video=*/true);
  test_api.PerformCapture();
  test_api.FlushRecordingServiceForTesting();
  WaitForMilliseconds(1000);
  ChromeCaptureModeDelegate::Get()->SetIsScreenCaptureLocked(true);
  const base::FilePath video_path = WaitForVideoFileToBeSaved();
  VerifyVideoFileAndDelete(video_path);
}

// TODO(b/273521375): Flaky on ChromeOS MSAN builds.
#if defined(MEMORY_SANITIZER)
#define MAYBE_InvalidDownloadsPath DISABLED_InvalidDownloadsPath
#else
#define MAYBE_InvalidDownloadsPath InvalidDownloadsPath
#endif
IN_PROC_BROWSER_TEST_F(RecordingServiceBrowserTest,
                       MAYBE_InvalidDownloadsPath) {
  auto* download_prefs =
      DownloadPrefs::FromBrowserContext(browser()->profile());
  const base::FilePath removable_path =
      ash::CrosDisksClient::GetRemovableDiskMountPoint();
  const base::FilePath invalid_path =
      removable_path.Append(FILE_PATH_LITERAL("backup"));
  download_prefs->SetDownloadPath(invalid_path);
  // The invalid path will still be accepted by the browser, but won't be used
  // by Capture Mode.
  EXPECT_EQ(invalid_path, download_prefs->DownloadPath());
  EXPECT_NE(invalid_path,
            ChromeCaptureModeDelegate::Get()->GetUserDefaultDownloadsFolder());
  ash::CaptureModeTestApi test_api;
  test_api.StartForFullscreen(/*for_video=*/true);
  FinishVideoRecordingTest(&test_api);
}

// -----------------------------------------------------------------------------
// GifRecordingBrowserTest:

class GifRecordingBrowserTest : public InProcessBrowserTest {
 public:
  GifRecordingBrowserTest()
      : scoped_feature_list_(ash::features::kGifRecording) {}
  ~GifRecordingBrowserTest() override = default;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Records a GIF image of a region that fills the entire screen, then attempts
// to decode the resulting file to verify the GIF encoding was successful.
IN_PROC_BROWSER_TEST_F(GifRecordingBrowserTest, SuccessfulEncodeDecode) {
  base::AddFeatureIdTagToTestResult(
      "screenplay-9c11ed80-9e97-482c-9562-450bd891732b");

  aura::Window* browser_window = browser()->window()->GetNativeWindow();
  ash::CaptureModeTestApi test_api;
  test_api.SetRecordingType(ash::RecordingType::kGif);
  test_api.SetUserSelectedRegion(browser_window->GetRootWindow()->bounds());

  test_api.StartForRegion(/*for_video=*/true);
  test_api.PerformCapture();
  WaitForMilliseconds(1000);
  EXPECT_TRUE(test_api.IsVideoRecordingInProgress());
  test_api.FlushRecordingServiceForTesting();

  WaitForMilliseconds(2000);

  test_api.StopVideoRecording();
  base::FilePath gif_file = WaitForVideoFileToBeSaved();
  VerifyGifImage(gif_file);
}