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

#include <stddef.h>

#include "base/base64.h"
#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/containers/span.h"
#include "base/environment.h"
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/test/test_timeouts.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h"
#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h"
#include "chrome/browser/media/webrtc/webrtc_browsertest_perf.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/infobars/content/content_infobar_manager.h"
#include "components/infobars/core/infobar.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "media/base/media_switches.h"
#include "media/media_buildflags.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/python_utils.h"
#include "testing/perf/perf_test.h"
#include "third_party/blink/public/common/features.h"
#include "ui/gl/gl_switches.h"

namespace {
std::string MakeLabel(const char* test_name, const std::string& video_codec) {
  std::string codec_label = video_codec.empty() ? "" : "_" + video_codec;
  return base::StringPrintf("%s%s", test_name, codec_label.c_str());
}
}  // namespace

static const base::FilePath::CharType kFrameAnalyzerExecutable[] =
#if BUILDFLAG(IS_WIN)
    FILE_PATH_LITERAL("frame_analyzer.exe");
#else
    FILE_PATH_LITERAL("frame_analyzer");
#endif

static const base::FilePath::CharType kCapturedYuvFileName[] =
    FILE_PATH_LITERAL("captured_video.yuv");
static const base::FilePath::CharType kCapturedWebmFileName[] =
    FILE_PATH_LITERAL("captured_video.webm");
static const char kMainWebrtcTestHtmlPage[] =
    "/webrtc/webrtc_jsep01_test.html";
static const char kCapturingWebrtcHtmlPage[] =
    "/webrtc/webrtc_video_quality_test.html";

static const struct VideoQualityTestConfig {
  const char* test_name;
  int width;
  int height;
  const base::FilePath::CharType* reference_video;
  const char* constraints;
} kVideoConfigurations[] = {
  { "360p", 640, 360,
    test::kReferenceFileName360p,
    WebRtcTestBase::kAudioVideoCallConstraints360p },
    { "720p", 1280, 720,
    test::kReferenceFileName720p,
    WebRtcTestBase::kAudioVideoCallConstraints720p },
};

// Test the video quality of the WebRTC output.
//
// Prerequisites: This test case must run on a machine with a chrome playing
// the video from the reference files located in GetReferenceFilesDir().
// The file kReferenceY4mFileName.kY4mFileExtension is played using a
// FileVideoCaptureDevice and its sibling with kYuvFileExtension is used for
// comparison.
//
// You must also compile the frame_analyzer target before you run this
// test to get all the tools built.
//
// The test runs several custom binaries - rgba_to_i420 converter and
// frame_analyzer. Both tools can be found under third_party/webrtc/rtc_tools.
// The test also runs a stand alone Python implementation of a WebSocket server
// (pywebsocket) and a barcode_decoder script.
class WebRtcVideoQualityBrowserTest : public WebRtcTestBase,
    public testing::WithParamInterface<VideoQualityTestConfig> {
 public:
  WebRtcVideoQualityBrowserTest()
      : environment_(base::Environment::Create()) {
    test_config_ = GetParam();
  }

  void SetUpInProcessBrowserTestFixture() override {
    DetectErrorsInJavaScript();  // Look for errors in our rather complex js.

    ASSERT_TRUE(temp_working_dir_.CreateUniqueTempDir());
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    // Set up the command line option with the expected file name. We will check
    // its existence in HasAllRequiredResources().
    webrtc_reference_video_y4m_ = test::GetReferenceFilesDir()
        .Append(test_config_.reference_video)
        .AddExtension(test::kY4mFileExtension);
    command_line->AppendSwitchPath(switches::kUseFileForFakeVideoCapture,
                                   webrtc_reference_video_y4m_);

    // The video playback will not work without a GPU, so force its use here.
    command_line->AppendSwitch(switches::kUseGpuInTests);
  }

  // Writes the captured video to a webm file.
  void WriteCapturedWebmVideo(content::WebContents* capturing_tab,
                              const base::FilePath& webm_video_filename) {
    std::string base64_encoded_video =
        ExecuteJavascript("getRecordedVideoAsBase64()", capturing_tab);
    std::string recorded_video;
    ASSERT_TRUE(base::Base64Decode(base64_encoded_video, &recorded_video));
    base::File video_file(webm_video_filename,
                          base::File::FLAG_CREATE | base::File::FLAG_WRITE);
    ASSERT_TRUE(
        video_file.WriteAndCheck(0, base::as_byte_span(recorded_video)));
  }

  // Runs ffmpeg on the captured webm video and writes it to a yuv video file.
  bool RunWebmToI420Converter(const base::FilePath& webm_video_filename,
                              const base::FilePath& yuv_video_filename,
                              const int width,
                              const int height) {
    base::FilePath path_to_ffmpeg = test::GetToolForPlatform("ffmpeg");
    if (!base::PathExists(path_to_ffmpeg)) {
      LOG(ERROR) << "Missing ffmpeg: should be in " << path_to_ffmpeg.value();
      return false;
    }

    // Set up ffmpeg to output at a certain resolution (-s) and bitrate (-b:v).
    // This is needed because WebRTC is free to start the call at a lower
    // resolution before ramping up. Without these flags, ffmpeg would output a
    // video in the initial lower resolution, causing the SSIM and PSNR results
    // to become meaningless.
    base::CommandLine ffmpeg_command(path_to_ffmpeg);
    ffmpeg_command.AppendArg("-i");
    ffmpeg_command.AppendArgPath(webm_video_filename);
    ffmpeg_command.AppendArg("-s");
    ffmpeg_command.AppendArg(base::StringPrintf("%dx%d", width, height));
    ffmpeg_command.AppendArg("-b:v");
    ffmpeg_command.AppendArg(base::StringPrintf("%d", 120 * width * height));
    ffmpeg_command.AppendArg("-vsync");
    ffmpeg_command.AppendArg("passthrough");
    ffmpeg_command.AppendArgPath(yuv_video_filename);

    // We produce an output file that will later be used as an input to the
    // barcode decoder and frame analyzer tools.
    DVLOG(0) << "Running " << ffmpeg_command.GetCommandLineString();
    std::string result;
    bool ok = base::GetAppOutputAndError(ffmpeg_command, &result);
    DVLOG(0) << "Output was:\n\n" << result;
    return ok;
  }

  // Compares the |captured_video_filename| with the |reference_video_filename|.
  //
  // The barcode decoder decodes the captured video containing barcodes overlaid
  // into every frame of the video. It produces a set of PNG images.
  // The frames should be of size |width| x |height|.
  // All measurements calculated are printed as perf parsable numbers to stdout.
  bool CompareVideosAndPrintResult(
      const std::string& test_label,
      int width,
      int height,
      const base::FilePath& captured_video_filename,
      const base::FilePath& reference_video_filename) {
    base::FilePath path_to_analyzer = base::MakeAbsoluteFilePath(
        GetTestBinaryDir().Append(kFrameAnalyzerExecutable));
    base::FilePath path_to_compare_script = GetSourceDir().Append(
        FILE_PATH_LITERAL("third_party/webrtc/rtc_tools/compare_videos.py"));

    if (!base::PathExists(path_to_analyzer)) {
      LOG(ERROR) << "Missing frame analyzer: should be in "
          << path_to_analyzer.value()
          << ". Try building the frame_analyzer target.";
      return false;
    }
    if (!base::PathExists(path_to_compare_script)) {
      LOG(ERROR) << "Missing video compare script: should be in "
          << path_to_compare_script.value();
      return false;
    }

    // Note: don't append switches to this command since it will mess up the
    // -u in the python invocation!
    base::CommandLine compare_command(base::CommandLine::NO_PROGRAM);
    EXPECT_TRUE(GetPython3Command(&compare_command));

    compare_command.AppendArgPath(path_to_compare_script);
    compare_command.AppendArg("--label=" + test_label);
    compare_command.AppendArg("--ref_video");
    compare_command.AppendArgPath(reference_video_filename);
    compare_command.AppendArg("--test_video");
    compare_command.AppendArgPath(captured_video_filename);
    compare_command.AppendArg("--frame_analyzer");
    compare_command.AppendArgPath(path_to_analyzer);
    compare_command.AppendArg("--yuv_frame_width");
    compare_command.AppendArg(base::NumberToString(width));
    compare_command.AppendArg("--yuv_frame_height");
    compare_command.AppendArg(base::NumberToString(height));

    DVLOG(0) << "Running " << compare_command.GetCommandLineString();
    std::string output;
    bool ok = base::GetAppOutput(compare_command, &output);

    // Print to stdout to ensure the perf numbers are parsed properly by the
    // buildbot step. The tool should print a handful RESULT lines.
    printf("Output was:\n\n%s\n", output.c_str());
    bool has_result_lines = output.find("RESULT") != std::string::npos;
    if (!ok || !has_result_lines) {
      LOG(ERROR) << "Failed to compare videos; see output to see what "
                 << "the error was:\n\n"
                 << output;
      return false;
    }
    // TODO(http://crbug.com/40610245): Enable this and drop the printf above
    // when ready to switch to histogram sets.
    // if (!test::WriteCompareVideosOutputAsHistogram(test_label, output))
    //  return false;

    return true;
  }

  void TestVideoQuality(const std::string& video_codec,
                        bool prefer_hw_video_codec) {
    ASSERT_GE(TestTimeouts::test_launcher_timeout().InSeconds(), 150)
        << "This is a long-running test; you must specify "
           "--test-launcher-timeout to have a value of at least 150000.";
    ASSERT_GE(TestTimeouts::action_max_timeout().InSeconds(), 150)
        << "This is a long-running test; you must specify "
           "--ui-test-action-max-timeout to have a value of at least 150000.";
    ASSERT_LT(TestTimeouts::action_max_timeout(),
              TestTimeouts::test_launcher_timeout())
        << "action_max_timeout needs to be strictly-less-than "
           "test_launcher_timeout";
    ASSERT_TRUE(test::HasReferenceFilesInCheckout());
    ASSERT_TRUE(embedded_test_server()->Start());

    content::WebContents* left_tab =
        OpenPageAndGetUserMediaInNewTabWithConstraints(
            embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage),
            test_config_.constraints);
    content::WebContents* right_tab =
        OpenPageAndGetUserMediaInNewTabWithConstraints(
            embedded_test_server()->GetURL(kCapturingWebrtcHtmlPage),
            test_config_.constraints);

    SetupPeerconnectionWithLocalStream(left_tab);
    SetupPeerconnectionWithLocalStream(right_tab);

    if (!video_codec.empty()) {
      SetDefaultVideoCodec(left_tab, video_codec, prefer_hw_video_codec);
      SetDefaultVideoCodec(right_tab, video_codec, prefer_hw_video_codec);
    }
    NegotiateCall(left_tab, right_tab);

    // Poll slower here to avoid flooding the log with messages: capturing and
    // sending frames take quite a bit of time.
    int polling_interval_msec = 1000;

    EXPECT_TRUE(test::PollingWaitUntil("doneFrameCapturing()", "done-capturing",
                                       right_tab, polling_interval_msec));

    HangUp(left_tab);

    WriteCapturedWebmVideo(right_tab,
                           GetWorkingDir().Append(kCapturedWebmFileName));

    // Shut everything down to avoid having the javascript race with the
    // analysis tools. For instance, dont have console log printouts interleave
    // with the RESULT lines from the analysis tools (crbug.com/40342719).
    chrome::CloseWebContents(browser(), left_tab, false);
    chrome::CloseWebContents(browser(), right_tab, false);

    RunWebmToI420Converter(GetWorkingDir().Append(kCapturedWebmFileName),
                           GetWorkingDir().Append(kCapturedYuvFileName),
                           test_config_.width, test_config_.height);

    ASSERT_TRUE(CompareVideosAndPrintResult(
        MakeLabel(test_config_.test_name, video_codec), test_config_.width,
        test_config_.height, GetWorkingDir().Append(kCapturedYuvFileName),
        test::GetReferenceFilesDir()
            .Append(test_config_.reference_video)
            .AddExtension(test::kYuvFileExtension)));
  }

 protected:
  VideoQualityTestConfig test_config_;

  base::FilePath GetWorkingDir() { return temp_working_dir_.GetPath(); }

 private:
  base::FilePath GetSourceDir() {
    base::FilePath source_dir;
    base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &source_dir);
    return source_dir;
  }

  base::FilePath GetTestBinaryDir() {
    base::FilePath browser_dir;
    EXPECT_TRUE(
        base::PathService::Get(base::DIR_OUT_TEST_DATA_ROOT, &browser_dir));
    return browser_dir;
  }

  std::unique_ptr<base::Environment> environment_;
  base::FilePath webrtc_reference_video_y4m_;
  base::ScopedTempDir temp_working_dir_;
};

INSTANTIATE_TEST_SUITE_P(WebRtcVideoQualityBrowserTests,
                         WebRtcVideoQualityBrowserTest,
                         testing::ValuesIn(kVideoConfigurations));

// WebRTC's frame_analyzer doesn't build from a Chromium's component build.
#if defined(COMPONENT_BUILD)
#define MAYBE_MANUAL_TestVideoQualityVp8 DISABLED_MANUAL_TestVideoQualityVp8
#else
#define MAYBE_MANUAL_TestVideoQualityVp8 MANUAL_TestVideoQualityVp8
#endif
IN_PROC_BROWSER_TEST_P(WebRtcVideoQualityBrowserTest,
                       MAYBE_MANUAL_TestVideoQualityVp8) {
  base::ScopedAllowBlockingForTesting allow_blocking;
  TestVideoQuality("VP8", false /* prefer_hw_video_codec */);
}

// Flaky on windows and WebRTC's frame_analyzer doesn't build from a Chromium's
// component build.
// TODO(crbug.com/40100787): re-enable when flakiness is investigated, diagnosed
// and resolved.
#if BUILDFLAG(IS_WIN) || defined(COMPONENT_BUILD)
#define MAYBE_MANUAL_TestVideoQualityVp9 DISABLED_MANUAL_TestVideoQualityVp9
#else
#define MAYBE_MANUAL_TestVideoQualityVp9 MANUAL_TestVideoQualityVp9
#endif
IN_PROC_BROWSER_TEST_P(WebRtcVideoQualityBrowserTest,
                       MAYBE_MANUAL_TestVideoQualityVp9) {
  base::ScopedAllowBlockingForTesting allow_blocking;
  TestVideoQuality("VP9", true /* prefer_hw_video_codec */);
}

#if BUILDFLAG(ENABLE_OPENH264)

// Flaky on mac (crbug.com/40534742) and WebRTC's frame_analyzer doesn't build
// from a Chromium's component build.
#if BUILDFLAG(IS_MAC) || defined(COMPONENT_BUILD)
#define MAYBE_MANUAL_TestVideoQualityH264 DISABLED_MANUAL_TestVideoQualityH264
#else
#define MAYBE_MANUAL_TestVideoQualityH264 MANUAL_TestVideoQualityH264
#endif

IN_PROC_BROWSER_TEST_P(WebRtcVideoQualityBrowserTest,
                       MAYBE_MANUAL_TestVideoQualityH264) {
  base::ScopedAllowBlockingForTesting allow_blocking;
  // Only run test if run-time feature corresponding to OpenH264 is on.
  if (!base::FeatureList::IsEnabled(media::kOpenH264SoftwareEncoder)) {
    LOG(WARNING)
        << "Run-time feature OpenH264SoftwareEncoder disabled. "
           "Skipping WebRtcVideoQualityBrowserTest.MANUAL_TestVideoQualityH264 "
           "(test \"OK\")";
    return;
  }
  TestVideoQuality("H264", true /* prefer_hw_video_codec */);
}

#endif  // BUILDFLAG(ENABLE_OPENH264)
