Revert "Ice Server API integration"
This reverts commit b23ba4d3fb
.
Reason for revert: Speculative revert for multiple failures in https://ci.chromium.org/p/chrome/builders/ci/linux-chromeos-chrome/2057
Original change's description:
> Ice Server API integration
>
> Integrates network traversal API to fetch stun and turn servers for
> webrtc p2p connection. Handles API failure by returning a list of
> public Google stun servers as a fallback.
>
> Bug: 1031156
> Change-Id: I60b5d6ccdb749ce9375bcb3333dc2cbdab62aa18
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1994295
> Commit-Queue: Himanshu Jaju <himanshujaju@chromium.org>
> Reviewed-by: Martin Šrámek <msramek@chromium.org>
> Reviewed-by: David Roger <droger@chromium.org>
> Reviewed-by: Richard Knoll <knollr@chromium.org>
> Reviewed-by: Alex Gough <ajgo@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#733941}
TBR=droger@chromium.org,msramek@chromium.org,knollr@chromium.org,himanshujaju@chromium.org,ajgo@chromium.org
Change-Id: I62270288afa5716c4dfb5fa6c35df2cfe0a82d79
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Bug: 1031156
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2013288
Reviewed-by: Olga Sharonova <olka@chromium.org>
Commit-Queue: Olga Sharonova <olka@chromium.org>
Cr-Commit-Position: refs/heads/master@{#733995}
This commit is contained in:

committed by
Commit Bot

parent
0b695eb43a
commit
896c950a7f
chrome
browser
test
google_apis
tools/traffic_annotation/summary
@ -3559,8 +3559,6 @@ jumbo_static_library("browser") {
|
||||
"sharing/sharing_notification_handler.h",
|
||||
"sharing/sharing_ui_controller.cc",
|
||||
"sharing/sharing_ui_controller.h",
|
||||
"sharing/webrtc/ice_config_fetcher.cc",
|
||||
"sharing/webrtc/ice_config_fetcher.h",
|
||||
"signin/signin_promo.cc",
|
||||
"signin/signin_promo.h",
|
||||
"signin/signin_ui_util.cc",
|
||||
@ -3715,7 +3713,6 @@ jumbo_static_library("browser") {
|
||||
"//chrome/services/app_service/public/cpp:icon_loader",
|
||||
"//chrome/services/app_service/public/cpp:intents",
|
||||
"//chrome/services/app_service/public/cpp:preferred_apps",
|
||||
"//chrome/services/sharing/public/mojom",
|
||||
"//components/feedback",
|
||||
"//components/image_fetcher/core",
|
||||
"//components/keep_alive_registry",
|
||||
|
@ -21,7 +21,6 @@ include_rules = [
|
||||
"+chrome/services/file_util/public",
|
||||
"+chrome/services/media_gallery_util/public",
|
||||
"+chrome/services/printing/public",
|
||||
"+chrome/services/sharing/public",
|
||||
"+chrome/services/removable_storage_writer/public",
|
||||
"+chrome/services/util_win/public",
|
||||
"+chromeos",
|
||||
|
@ -1,164 +0,0 @@
|
||||
// Copyright 2020 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 "chrome/browser/sharing/webrtc/ice_config_fetcher.h"
|
||||
|
||||
#include "base/bind.h"
|
||||
#include "base/json/json_reader.h"
|
||||
#include "base/optional.h"
|
||||
#include "base/strings/strcat.h"
|
||||
#include "google_apis/google_api_keys.h"
|
||||
#include "net/base/load_flags.h"
|
||||
#include "services/network/public/cpp/shared_url_loader_factory.h"
|
||||
#include "services/network/public/cpp/simple_url_loader.h"
|
||||
|
||||
namespace {
|
||||
const char kIceConfigApiUrl[] =
|
||||
"https://networktraversal.googleapis.com/v1alpha/iceconfig?key=";
|
||||
|
||||
// Response with 2 ice server configs takes ~1KB. A loose upper bound of 16KB is
|
||||
// chosen to avoid breaking the flow in case the response has longer URLs in ice
|
||||
// configs.
|
||||
constexpr int kMaxBodySize = 16 * 1024;
|
||||
|
||||
const net::NetworkTrafficAnnotationTag kTrafficAnnotation =
|
||||
net::DefineNetworkTrafficAnnotation("ice_config_fetcher", R"(
|
||||
semantics {
|
||||
sender: "IceConfigFetcher"
|
||||
description:
|
||||
"Fetches ice server configurations for p2p webrtc connection as "
|
||||
"described in "
|
||||
"https://www.w3.org/TR/webrtc/#rtciceserver-dictionary."
|
||||
trigger:
|
||||
"User uses any Chrome cross-device sharing feature and selects one"
|
||||
" of their devices to send the data to."
|
||||
data: "No data is sent in the request."
|
||||
destination: GOOGLE_OWNED_SERVICE
|
||||
}
|
||||
policy {
|
||||
cookies_allowed: NO
|
||||
setting:
|
||||
"Users can disable this behavior by signing out of Chrome."
|
||||
chrome_policy {
|
||||
BrowserSignin {
|
||||
policy_options {mode: MANDATORY}
|
||||
BrowserSignin: 0
|
||||
}
|
||||
}
|
||||
})");
|
||||
|
||||
bool IsLoaderSuccessful(const network::SimpleURLLoader* loader) {
|
||||
if (!loader || loader->NetError() != net::OK)
|
||||
return false;
|
||||
|
||||
if (!loader->ResponseInfo() || !loader->ResponseInfo()->headers)
|
||||
return false;
|
||||
|
||||
// Success response codes are 2xx.
|
||||
return (loader->ResponseInfo()->headers->response_code() / 100) == 2;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
IceConfigFetcher::IceConfigFetcher(
|
||||
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
|
||||
: url_loader_factory_(std::move(url_loader_factory)) {}
|
||||
|
||||
IceConfigFetcher::~IceConfigFetcher() = default;
|
||||
|
||||
void IceConfigFetcher::GetIceServers(IceServerCallback callback) {
|
||||
url_loader_.reset();
|
||||
|
||||
auto resource_request = std::make_unique<network::ResourceRequest>();
|
||||
resource_request->url =
|
||||
GURL(base::StrCat({kIceConfigApiUrl, google_apis::GetSharingAPIKey()}));
|
||||
resource_request->load_flags =
|
||||
net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE;
|
||||
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
|
||||
resource_request->method = net::HttpRequestHeaders::kPostMethod;
|
||||
resource_request->headers.SetHeader(net::HttpRequestHeaders::kContentType,
|
||||
"application/json");
|
||||
|
||||
url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
|
||||
kTrafficAnnotation);
|
||||
url_loader_->DownloadToString(
|
||||
url_loader_factory_.get(),
|
||||
base::BindOnce(&IceConfigFetcher::OnIceServersResponse,
|
||||
weak_ptr_factory_.GetWeakPtr(), std::move(callback)),
|
||||
kMaxBodySize);
|
||||
}
|
||||
|
||||
void IceConfigFetcher::OnIceServersResponse(
|
||||
IceServerCallback callback,
|
||||
std::unique_ptr<std::string> response_body) {
|
||||
std::vector<sharing::mojom::IceServerPtr> ice_servers;
|
||||
|
||||
if (IsLoaderSuccessful(url_loader_.get()) && response_body)
|
||||
ice_servers = ParseIceConfigJson(*response_body);
|
||||
|
||||
if (ice_servers.empty())
|
||||
ice_servers = GetDefaultIceServers();
|
||||
|
||||
std::move(callback).Run(std::move(ice_servers));
|
||||
}
|
||||
|
||||
std::vector<sharing::mojom::IceServerPtr> IceConfigFetcher::ParseIceConfigJson(
|
||||
std::string json) {
|
||||
std::vector<sharing::mojom::IceServerPtr> ice_servers;
|
||||
base::Optional<base::Value> response = base::JSONReader::Read(json);
|
||||
if (!response)
|
||||
return ice_servers;
|
||||
|
||||
base::Value* ice_servers_json = response->FindListKey("iceServers");
|
||||
if (!ice_servers_json)
|
||||
return ice_servers;
|
||||
|
||||
for (base::Value& server : ice_servers_json->GetList()) {
|
||||
const base::Value* urls_json = server.FindListKey("urls");
|
||||
if (!urls_json)
|
||||
continue;
|
||||
|
||||
std::vector<GURL> urls;
|
||||
for (const base::Value& url_json : urls_json->GetList()) {
|
||||
std::string url;
|
||||
if (!url_json.GetAsString(&url))
|
||||
continue;
|
||||
|
||||
urls.emplace_back(url);
|
||||
}
|
||||
|
||||
if (urls.empty())
|
||||
continue;
|
||||
|
||||
sharing::mojom::IceServerPtr ice_server(sharing::mojom::IceServer::New());
|
||||
ice_server->urls = std::move(urls);
|
||||
|
||||
std::string* retrieved_username = server.FindStringKey("username");
|
||||
if (retrieved_username)
|
||||
ice_server->username.emplace(std::move(*retrieved_username));
|
||||
|
||||
std::string* retrieved_credential = server.FindStringKey("credential");
|
||||
if (retrieved_credential)
|
||||
ice_server->credential.emplace(std::move(*retrieved_credential));
|
||||
|
||||
ice_servers.push_back(std::move(ice_server));
|
||||
}
|
||||
|
||||
return ice_servers;
|
||||
}
|
||||
|
||||
// static
|
||||
std::vector<sharing::mojom::IceServerPtr>
|
||||
IceConfigFetcher::GetDefaultIceServers() {
|
||||
sharing::mojom::IceServerPtr ice_server(sharing::mojom::IceServer::New());
|
||||
ice_server->urls.emplace_back("stun:stun.l.google.com:19302");
|
||||
ice_server->urls.emplace_back("stun:stun1.l.google.com:19302");
|
||||
ice_server->urls.emplace_back("stun:stun2.l.google.com:19302");
|
||||
ice_server->urls.emplace_back("stun:stun3.l.google.com:19302");
|
||||
ice_server->urls.emplace_back("stun:stun4.l.google.com:19302");
|
||||
|
||||
std::vector<sharing::mojom::IceServerPtr> default_servers;
|
||||
default_servers.push_back(std::move(ice_server));
|
||||
return default_servers;
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
// Copyright 2020 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.
|
||||
|
||||
#ifndef CHROME_BROWSER_SHARING_WEBRTC_ICE_CONFIG_FETCHER_H_
|
||||
#define CHROME_BROWSER_SHARING_WEBRTC_ICE_CONFIG_FETCHER_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/callback_forward.h"
|
||||
#include "base/memory/scoped_refptr.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "base/optional.h"
|
||||
#include "chrome/services/sharing/public/mojom/webrtc.mojom.h"
|
||||
#include "url/gurl.h"
|
||||
|
||||
namespace network {
|
||||
class SharedURLLoaderFactory;
|
||||
class SimpleURLLoader;
|
||||
} // namespace network
|
||||
|
||||
class IceConfigFetcher {
|
||||
public:
|
||||
using IceServerCallback =
|
||||
base::OnceCallback<void(std::vector<sharing::mojom::IceServerPtr>)>;
|
||||
|
||||
explicit IceConfigFetcher(
|
||||
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory);
|
||||
~IceConfigFetcher();
|
||||
|
||||
IceConfigFetcher(const IceConfigFetcher& other) = delete;
|
||||
IceConfigFetcher& operator=(const IceConfigFetcher& other) = delete;
|
||||
|
||||
// TODO(himanshujaju) - Cache configs fetched from server.
|
||||
void GetIceServers(IceServerCallback callback);
|
||||
|
||||
private:
|
||||
void OnIceServersResponse(IceServerCallback callback,
|
||||
std::unique_ptr<std::string> response_body);
|
||||
|
||||
std::vector<sharing::mojom::IceServerPtr> ParseIceConfigJson(
|
||||
std::string json);
|
||||
|
||||
// Returns public ice servers if API fails to respond.
|
||||
static std::vector<sharing::mojom::IceServerPtr> GetDefaultIceServers();
|
||||
|
||||
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
|
||||
std::unique_ptr<network::SimpleURLLoader> url_loader_;
|
||||
|
||||
base::WeakPtrFactory<IceConfigFetcher> weak_ptr_factory_{this};
|
||||
};
|
||||
|
||||
#endif // CHROME_BROWSER_SHARING_WEBRTC_ICE_CONFIG_FETCHER_H_
|
@ -1,102 +0,0 @@
|
||||
// Copyright 2020 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 "chrome/browser/sharing/webrtc/ice_config_fetcher.h"
|
||||
|
||||
#include "base/files/file_util.h"
|
||||
#include "base/path_service.h"
|
||||
#include "base/run_loop.h"
|
||||
#include "base/strings/strcat.h"
|
||||
#include "base/test/bind_test_util.h"
|
||||
#include "base/test/task_environment.h"
|
||||
#include "chrome/common/chrome_paths.h"
|
||||
#include "google_apis/google_api_keys.h"
|
||||
#include "services/network/public/cpp/shared_url_loader_factory.h"
|
||||
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
|
||||
#include "services/network/test/test_url_loader_factory.h"
|
||||
#include "testing/gmock/include/gmock/gmock.h"
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
|
||||
class IceConfigFetcherTest : public testing::Test {
|
||||
public:
|
||||
IceConfigFetcherTest()
|
||||
: test_shared_loader_factory_(
|
||||
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
|
||||
&test_url_loader_factory_)),
|
||||
ice_config_fetcher_(test_shared_loader_factory_) {}
|
||||
~IceConfigFetcherTest() override = default;
|
||||
|
||||
std::string GetApiUrl() const {
|
||||
return base::StrCat(
|
||||
{"https://networktraversal.googleapis.com/v1alpha/iceconfig?key=",
|
||||
google_apis::GetSharingAPIKey()});
|
||||
}
|
||||
|
||||
void CheckSuccessResponse(
|
||||
const std::vector<sharing::mojom::IceServerPtr>& ice_servers) {
|
||||
ASSERT_EQ(2u, ice_servers.size());
|
||||
|
||||
// First response doesnt have credentials.
|
||||
ASSERT_EQ(1u, ice_servers[0]->urls.size());
|
||||
ASSERT_FALSE(ice_servers[0]->username);
|
||||
ASSERT_FALSE(ice_servers[0]->credential);
|
||||
|
||||
// Second response has credentials.
|
||||
ASSERT_EQ(2u, ice_servers[1]->urls.size());
|
||||
ASSERT_EQ("username", ice_servers[1]->username);
|
||||
ASSERT_EQ("credential", ice_servers[1]->credential);
|
||||
}
|
||||
|
||||
void GetSuccessResponse(std::string* response) const {
|
||||
base::FilePath path;
|
||||
ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &path));
|
||||
path = path.AppendASCII("sharing");
|
||||
ASSERT_TRUE(base::PathExists(path));
|
||||
ASSERT_TRUE(base::ReadFileToString(
|
||||
path.AppendASCII("network_traversal_response.json"), response));
|
||||
}
|
||||
|
||||
protected:
|
||||
base::test::SingleThreadTaskEnvironment task_environment_;
|
||||
network::TestURLLoaderFactory test_url_loader_factory_;
|
||||
scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_;
|
||||
IceConfigFetcher ice_config_fetcher_;
|
||||
};
|
||||
|
||||
TEST_F(IceConfigFetcherTest, ResponseSuccessful) {
|
||||
base::RunLoop run_loop;
|
||||
ice_config_fetcher_.GetIceServers(base::BindLambdaForTesting(
|
||||
[&](std::vector<sharing::mojom::IceServerPtr> ice_servers) {
|
||||
CheckSuccessResponse(ice_servers);
|
||||
run_loop.Quit();
|
||||
}));
|
||||
|
||||
const std::string expected_api_url = GetApiUrl();
|
||||
std::string response;
|
||||
GetSuccessResponse(&response);
|
||||
|
||||
ASSERT_TRUE(test_url_loader_factory_.IsPending(expected_api_url, nullptr));
|
||||
|
||||
test_url_loader_factory_.AddResponse(expected_api_url, response,
|
||||
net::HTTP_OK);
|
||||
run_loop.Run();
|
||||
}
|
||||
|
||||
TEST_F(IceConfigFetcherTest, ResponseError) {
|
||||
base::RunLoop run_loop;
|
||||
ice_config_fetcher_.GetIceServers(base::BindLambdaForTesting(
|
||||
[&](std::vector<sharing::mojom::IceServerPtr> ice_servers) {
|
||||
// Makes sure that we at least return default servers in case of an
|
||||
// error.
|
||||
EXPECT_FALSE(ice_servers.empty());
|
||||
run_loop.Quit();
|
||||
}));
|
||||
|
||||
const std::string expected_api_url = GetApiUrl();
|
||||
ASSERT_TRUE(test_url_loader_factory_.IsPending(expected_api_url, nullptr));
|
||||
|
||||
test_url_loader_factory_.AddResponse(expected_api_url, "",
|
||||
net::HTTP_INTERNAL_SERVER_ERROR);
|
||||
run_loop.Run();
|
||||
}
|
@ -4193,7 +4193,6 @@ test("unit_tests") {
|
||||
"../browser/sharing/shared_clipboard/shared_clipboard_utils_unittest.cc",
|
||||
"../browser/sharing/sms/sms_fetch_request_handler_unittest.cc",
|
||||
"../browser/sharing/sms/sms_remote_fetcher_unittest.cc",
|
||||
"../browser/sharing/webrtc/ice_config_fetcher_unittest.cc",
|
||||
"../browser/ui/autofill/payments/local_card_migration_bubble_controller_impl_unittest.cc",
|
||||
"../browser/ui/autofill/payments/save_card_bubble_controller_impl_unittest.cc",
|
||||
"../browser/ui/bluetooth/bluetooth_chooser_controller_unittest.cc",
|
||||
|
@ -1,21 +0,0 @@
|
||||
{
|
||||
"lifetimeDuration": "400s",
|
||||
"iceServers": [
|
||||
{
|
||||
"urls": [
|
||||
"stun:url1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"urls": [
|
||||
"turn:url2?transport=udp",
|
||||
"turn:url3?transport=tcp"
|
||||
],
|
||||
"username": "username",
|
||||
"credential": "credential",
|
||||
"maxRateKbps": "1"
|
||||
}
|
||||
],
|
||||
"blockStatus": "NOT_BLOCKED",
|
||||
"iceTransportPolicy": "all"
|
||||
}
|
@ -83,11 +83,6 @@
|
||||
#define GOOGLE_API_KEY_REMOTING DUMMY_API_TOKEN
|
||||
#endif
|
||||
|
||||
// API key for SharingService.
|
||||
#if !defined(GOOGLE_API_KEY_SHARING)
|
||||
#define GOOGLE_API_KEY_SHARING DUMMY_API_TOKEN
|
||||
#endif
|
||||
|
||||
// These are used as shortcuts for developers and users providing
|
||||
// OAuth credentials via preprocessor defines or environment
|
||||
// variables. If set, they will be used to replace any of the client
|
||||
@ -131,10 +126,6 @@ class APIKeyCache {
|
||||
STRINGIZE_NO_EXPANSION(GOOGLE_API_KEY_REMOTING), nullptr, std::string(),
|
||||
environment.get(), command_line);
|
||||
|
||||
api_key_sharing_ = CalculateKeyValue(
|
||||
GOOGLE_API_KEY_SHARING, STRINGIZE_NO_EXPANSION(GOOGLE_API_KEY_SHARING),
|
||||
nullptr, std::string(), environment.get(), command_line);
|
||||
|
||||
metrics_key_ = CalculateKeyValue(
|
||||
GOOGLE_METRICS_SIGNING_KEY,
|
||||
STRINGIZE_NO_EXPANSION(GOOGLE_METRICS_SIGNING_KEY), nullptr,
|
||||
@ -204,7 +195,6 @@ class APIKeyCache {
|
||||
#endif
|
||||
std::string api_key_non_stable() const { return api_key_non_stable_; }
|
||||
std::string api_key_remoting() const { return api_key_remoting_; }
|
||||
std::string api_key_sharing() const { return api_key_sharing_; }
|
||||
|
||||
std::string metrics_key() const { return metrics_key_; }
|
||||
|
||||
@ -303,7 +293,6 @@ class APIKeyCache {
|
||||
std::string api_key_;
|
||||
std::string api_key_non_stable_;
|
||||
std::string api_key_remoting_;
|
||||
std::string api_key_sharing_;
|
||||
std::string metrics_key_;
|
||||
std::string client_ids_[CLIENT_NUM_ITEMS];
|
||||
std::string client_secrets_[CLIENT_NUM_ITEMS];
|
||||
@ -328,10 +317,6 @@ std::string GetRemotingAPIKey() {
|
||||
return g_api_key_cache.Get().api_key_remoting();
|
||||
}
|
||||
|
||||
std::string GetSharingAPIKey() {
|
||||
return g_api_key_cache.Get().api_key_sharing();
|
||||
}
|
||||
|
||||
#if defined(OS_IOS)
|
||||
void SetAPIKey(const std::string& api_key) {
|
||||
g_api_key_cache.Get().set_api_key(api_key);
|
||||
|
@ -76,9 +76,6 @@ std::string GetNonStableAPIKey();
|
||||
// Retrieves the Chrome Remote Desktop API key.
|
||||
std::string GetRemotingAPIKey();
|
||||
|
||||
// Retrieves the Sharing API Key.
|
||||
std::string GetSharingAPIKey();
|
||||
|
||||
#if defined(OS_IOS)
|
||||
// Sets the API key. This should be called as early as possible before this
|
||||
// API key is even accessed.
|
||||
|
@ -136,7 +136,6 @@ Refer to README.md for content description and update process.
|
||||
<item id="history_ui_favicon_request_handler_get_favicon" hash_code="17562717" type="0" content_hash_code="64054629" os_list="linux,windows" file_path="components/favicon/core/history_ui_favicon_request_handler_impl.cc"/>
|
||||
<item id="http_server_error_response" hash_code="32197336" type="0" content_hash_code="61082230" os_list="linux,windows" file_path="net/server/http_server.cc"/>
|
||||
<item id="https_server_previews_navigation" hash_code="35725390" type="0" content_hash_code="84423109" os_list="linux,windows" file_path="chrome/browser/previews/previews_lite_page_redirect_serving_url_loader.cc"/>
|
||||
<item id="ice_config_fetcher" hash_code="137093034" type="0" content_hash_code="60051202" os_list="linux,windows" file_path="chrome/browser/sharing/webrtc/ice_config_fetcher.cc"/>
|
||||
<item id="icon_cacher" hash_code="103133150" type="0" content_hash_code="116368348" os_list="linux,windows" file_path="components/ntp_tiles/icon_cacher_impl.cc"/>
|
||||
<item id="icon_catcher_get_large_icon" hash_code="44494884" type="0" content_hash_code="98262037" os_list="linux,windows" file_path="components/ntp_tiles/icon_cacher_impl.cc"/>
|
||||
<item id="image_annotation" hash_code="107881858" type="0" content_hash_code="96203979" os_list="linux,windows" file_path="services/image_annotation/annotator.cc"/>
|
||||
|
Reference in New Issue
Block a user