From 8d01442d75a98a03cf1c8ef527ffaa87b0659e31 Mon Sep 17 00:00:00 2001 From: Mike Wiedenbauer Date: Fri, 1 May 2020 18:18:18 +0000 Subject: [PATCH] Re-implement audio capturing based on the AudioService API (fixes issue #2755) --- BUILD.gn | 2 + cef_paths.gypi | 8 +- cef_paths2.gypi | 2 + include/capi/cef_audio_handler_capi.h | 121 +++ include/capi/cef_client_capi.h | 9 +- include/cef_audio_handler.h | 111 +++ include/cef_client.h | 7 + include/internal/cef_types.h | 131 +++ include/internal/cef_types_wrappers.h | 19 + libcef/browser/audio_capturer.cc | 125 +++ libcef/browser/audio_capturer.h | 53 + libcef/browser/browser_host_impl.cc | 46 + libcef/browser/browser_host_impl.h | 15 + libcef_dll/cpptoc/audio_handler_cpptoc.cc | 191 ++++ libcef_dll/cpptoc/audio_handler_cpptoc.h | 37 + libcef_dll/cpptoc/client_cpptoc.cc | 20 +- libcef_dll/ctocpp/audio_handler_ctocpp.cc | 164 ++++ libcef_dll/ctocpp/audio_handler_ctocpp.h | 51 + libcef_dll/ctocpp/client_ctocpp.cc | 18 +- libcef_dll/ctocpp/client_ctocpp.h | 3 +- libcef_dll/wrapper_types.h | 3 +- tests/ceftests/audio_output_unittest.cc | 1074 +++++++++++++++++++++ tests/ceftests/client_app_delegates.cc | 4 + tools/cef_parser.py | 2 + 24 files changed, 2210 insertions(+), 6 deletions(-) create mode 100644 include/capi/cef_audio_handler_capi.h create mode 100644 include/cef_audio_handler.h create mode 100644 libcef/browser/audio_capturer.cc create mode 100644 libcef/browser/audio_capturer.h create mode 100644 libcef_dll/cpptoc/audio_handler_cpptoc.cc create mode 100644 libcef_dll/cpptoc/audio_handler_cpptoc.h create mode 100644 libcef_dll/ctocpp/audio_handler_ctocpp.cc create mode 100644 libcef_dll/ctocpp/audio_handler_ctocpp.h create mode 100644 tests/ceftests/audio_output_unittest.cc diff --git a/BUILD.gn b/BUILD.gn index 9eee58953..38afec936 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -377,6 +377,8 @@ if (is_win) { static_library("libcef_static") { sources = includes_common + gypi_paths.autogen_cpp_includes + [ + "libcef/browser/audio_capturer.cc", + "libcef/browser/audio_capturer.h", "libcef/browser/browser_context.cc", "libcef/browser/browser_context.h", "libcef/browser/browser_context_keyed_service_factories.cc", diff --git a/cef_paths.gypi b/cef_paths.gypi index 8d6ed0daa..c0322f135 100644 --- a/cef_paths.gypi +++ b/cef_paths.gypi @@ -8,7 +8,7 @@ # by hand. See the translator.README.txt file in the tools directory for # more information. # -# $hash=578c0aef11c3c7840679e480069fc9031c628e25$ +# $hash=21f0ab1e9902e4a47bf2893a4a383d33bd8161e2$ # { @@ -16,6 +16,7 @@ 'autogen_cpp_includes': [ 'include/cef_accessibility_handler.h', 'include/cef_app.h', + 'include/cef_audio_handler.h', 'include/cef_auth_callback.h', 'include/cef_browser.h', 'include/cef_browser_process_handler.h', @@ -111,6 +112,7 @@ 'autogen_capi_includes': [ 'include/capi/cef_accessibility_handler_capi.h', 'include/capi/cef_app_capi.h', + 'include/capi/cef_audio_handler_capi.h', 'include/capi/cef_auth_callback_capi.h', 'include/capi/cef_browser_capi.h', 'include/capi/cef_browser_process_handler_capi.h', @@ -208,6 +210,8 @@ 'libcef_dll/ctocpp/accessibility_handler_ctocpp.h', 'libcef_dll/ctocpp/app_ctocpp.cc', 'libcef_dll/ctocpp/app_ctocpp.h', + 'libcef_dll/ctocpp/audio_handler_ctocpp.cc', + 'libcef_dll/ctocpp/audio_handler_ctocpp.h', 'libcef_dll/cpptoc/auth_callback_cpptoc.cc', 'libcef_dll/cpptoc/auth_callback_cpptoc.h', 'libcef_dll/cpptoc/before_download_callback_cpptoc.cc', @@ -512,6 +516,8 @@ 'libcef_dll/cpptoc/accessibility_handler_cpptoc.h', 'libcef_dll/cpptoc/app_cpptoc.cc', 'libcef_dll/cpptoc/app_cpptoc.h', + 'libcef_dll/cpptoc/audio_handler_cpptoc.cc', + 'libcef_dll/cpptoc/audio_handler_cpptoc.h', 'libcef_dll/ctocpp/auth_callback_ctocpp.cc', 'libcef_dll/ctocpp/auth_callback_ctocpp.h', 'libcef_dll/ctocpp/before_download_callback_ctocpp.cc', diff --git a/cef_paths2.gypi b/cef_paths2.gypi index 5be1dddd4..aa3717f59 100644 --- a/cef_paths2.gypi +++ b/cef_paths2.gypi @@ -466,6 +466,7 @@ 'tests/cefsimple/simple_handler_linux.cc', ], 'ceftests_sources_common': [ + 'tests/ceftests/audio_output_unittest.cc', 'tests/ceftests/browser_info_map_unittest.cc', 'tests/ceftests/command_line_unittest.cc', 'tests/ceftests/cookie_unittest.cc', @@ -565,6 +566,7 @@ 'tests/shared/browser/resource_util.h', 'tests/shared/browser/resource_util_mac.mm', 'tests/shared/browser/resource_util_posix.cc', + 'tests/ceftests/audio_output_unittest.cc', 'tests/ceftests/client_app_delegates.cc', 'tests/ceftests/cookie_unittest.cc', 'tests/ceftests/dom_unittest.cc', diff --git a/include/capi/cef_audio_handler_capi.h b/include/capi/cef_audio_handler_capi.h new file mode 100644 index 000000000..1f6783b7d --- /dev/null +++ b/include/capi/cef_audio_handler_capi.h @@ -0,0 +1,121 @@ +// Copyright (c) 2020 Marshall A. Greenblatt. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the name Chromium Embedded +// Framework nor the names of its contributors may be used to endorse +// or promote products derived from this software without specific prior +// written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// --------------------------------------------------------------------------- +// +// This file was generated by the CEF translator tool and should not edited +// by hand. See the translator.README.txt file in the tools directory for +// more information. +// +// $hash=430877d950508a545d0baa18c8c8c0d2d183fec4$ +// + +#ifndef CEF_INCLUDE_CAPI_CEF_AUDIO_HANDLER_CAPI_H_ +#define CEF_INCLUDE_CAPI_CEF_AUDIO_HANDLER_CAPI_H_ +#pragma once + +#include "include/capi/cef_base_capi.h" +#include "include/capi/cef_browser_capi.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/// +// Implement this structure to handle audio events. +/// +typedef struct _cef_audio_handler_t { + /// + // Base structure. + /// + cef_base_ref_counted_t base; + + /// + // Called on the UI thread to allow configuration of audio stream parameters. + // Return true (1) to proceed with audio stream capture, or false (0) to + // cancel it. All members of |params| can optionally be configured here, but + // they are also pre-filled with some sensible defaults. + /// + int(CEF_CALLBACK* get_audio_parameters)(struct _cef_audio_handler_t* self, + struct _cef_browser_t* browser, + cef_audio_parameters_t* params); + + /// + // Called on a browser audio capture thread when the browser starts streaming + // audio. OnAudioSteamStopped will always be called after + // OnAudioStreamStarted; both functions may be called multiple times for the + // same browser. |params| contains the audio parameters like sample rate and + // channel layout. |channels| is the number of channels. + /// + void(CEF_CALLBACK* on_audio_stream_started)( + struct _cef_audio_handler_t* self, + struct _cef_browser_t* browser, + const cef_audio_parameters_t* params, + int channels); + + /// + // Called on the audio stream thread when a PCM packet is received for the + // stream. |data| is an array representing the raw PCM data as a floating + // point type, i.e. 4-byte value(s). |frames| is the number of frames in the + // PCM packet. |pts| is the presentation timestamp (in milliseconds since the + // Unix Epoch) and represents the time at which the decompressed packet should + // be presented to the user. Based on |frames| and the |channel_layout| value + // passed to OnAudioStreamStarted you can calculate the size of the |data| + // array in bytes. + /// + void(CEF_CALLBACK* on_audio_stream_packet)(struct _cef_audio_handler_t* self, + struct _cef_browser_t* browser, + const float** data, + int frames, + int64 pts); + + /// + // Called on the UI thread when the stream has stopped. OnAudioSteamStopped + // will always be called after OnAudioStreamStarted; both functions may be + // called multiple times for the same stream. + /// + void(CEF_CALLBACK* on_audio_stream_stopped)(struct _cef_audio_handler_t* self, + struct _cef_browser_t* browser); + + /// + // Called on the UI or audio stream thread when an error occurred. During the + // stream creation phase this callback will be called on the UI thread while + // in the capturing phase it will be called on the audio stream thread. The + // stream will be stopped immediately. + /// + void(CEF_CALLBACK* on_audio_stream_error)(struct _cef_audio_handler_t* self, + struct _cef_browser_t* browser, + const cef_string_t* message); +} cef_audio_handler_t; + +#ifdef __cplusplus +} +#endif + +#endif // CEF_INCLUDE_CAPI_CEF_AUDIO_HANDLER_CAPI_H_ diff --git a/include/capi/cef_client_capi.h b/include/capi/cef_client_capi.h index 4c2d31202..a7eb509a2 100644 --- a/include/capi/cef_client_capi.h +++ b/include/capi/cef_client_capi.h @@ -33,13 +33,14 @@ // by hand. See the translator.README.txt file in the tools directory for // more information. // -// $hash=6a0312765614a697d56e87c8503afba8404bb08b$ +// $hash=8d4cb3e0bbf230804c93898daa4a8b2866a2c1ce$ // #ifndef CEF_INCLUDE_CAPI_CEF_CLIENT_CAPI_H_ #define CEF_INCLUDE_CAPI_CEF_CLIENT_CAPI_H_ #pragma once +#include "include/capi/cef_audio_handler_capi.h" #include "include/capi/cef_base_capi.h" #include "include/capi/cef_context_menu_handler_capi.h" #include "include/capi/cef_dialog_handler_capi.h" @@ -69,6 +70,12 @@ typedef struct _cef_client_t { /// cef_base_ref_counted_t base; + /// + // Return the handler for audio rendering events. + /// + struct _cef_audio_handler_t*(CEF_CALLBACK* get_audio_handler)( + struct _cef_client_t* self); + /// // Return the handler for context menus. If no handler is provided the default // implementation will be used. diff --git a/include/cef_audio_handler.h b/include/cef_audio_handler.h new file mode 100644 index 000000000..f581e6f42 --- /dev/null +++ b/include/cef_audio_handler.h @@ -0,0 +1,111 @@ +// Copyright (c) 2019 Marshall A. Greenblatt. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the name Chromium Embedded +// Framework nor the names of its contributors may be used to endorse +// or promote products derived from this software without specific prior +// written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// --------------------------------------------------------------------------- +// +// The contents of this file must follow a specific format in order to +// support the CEF translator tool. See the translator.README.txt file in the +// tools directory for more information. +// + +#ifndef CEF_INCLUDE_CEF_AUDIO_HANDLER_H_ +#define CEF_INCLUDE_CEF_AUDIO_HANDLER_H_ +#pragma once + +#include "include/cef_base.h" +#include "include/cef_browser.h" + +/// +// Implement this interface to handle audio events. +/// +/*--cef(source=client)--*/ +class CefAudioHandler : public virtual CefBaseRefCounted { + public: + typedef cef_channel_layout_t ChannelLayout; + + /// + // Called on the UI thread to allow configuration of audio stream parameters. + // Return true to proceed with audio stream capture, or false to cancel it. + // All members of |params| can optionally be configured here, but they are + // also pre-filled with some sensible defaults. + /// + /*--cef()--*/ + virtual bool GetAudioParameters(CefRefPtr browser, + CefAudioParameters& params) { + return true; + } + + /// + // Called on a browser audio capture thread when the browser starts + // streaming audio. OnAudioSteamStopped will always be called after + // OnAudioStreamStarted; both methods may be called multiple times + // for the same browser. |params| contains the audio parameters like + // sample rate and channel layout. |channels| is the number of channels. + /// + /*--cef()--*/ + virtual void OnAudioStreamStarted(CefRefPtr browser, + const CefAudioParameters& params, + int channels) = 0; + + /// + // Called on the audio stream thread when a PCM packet is received for the + // stream. |data| is an array representing the raw PCM data as a floating + // point type, i.e. 4-byte value(s). |frames| is the number of frames in the + // PCM packet. |pts| is the presentation timestamp (in milliseconds since the + // Unix Epoch) and represents the time at which the decompressed packet should + // be presented to the user. Based on |frames| and the |channel_layout| value + // passed to OnAudioStreamStarted you can calculate the size of the |data| + // array in bytes. + /// + /*--cef()--*/ + virtual void OnAudioStreamPacket(CefRefPtr browser, + const float** data, + int frames, + int64 pts) = 0; + + /// + // Called on the UI thread when the stream has stopped. OnAudioSteamStopped + // will always be called after OnAudioStreamStarted; both methods may be + // called multiple times for the same stream. + /// + /*--cef()--*/ + virtual void OnAudioStreamStopped(CefRefPtr browser) = 0; + + /// + // Called on the UI or audio stream thread when an error occurred. During the + // stream creation phase this callback will be called on the UI thread while + // in the capturing phase it will be called on the audio stream thread. The + // stream will be stopped immediately. + /// + /*--cef()--*/ + virtual void OnAudioStreamError(CefRefPtr browser, + const CefString& message) = 0; +}; + +#endif // CEF_INCLUDE_CEF_AUDIO_HANDLER_H_ diff --git a/include/cef_client.h b/include/cef_client.h index 82fb0ef93..ada7a6a86 100644 --- a/include/cef_client.h +++ b/include/cef_client.h @@ -38,6 +38,7 @@ #define CEF_INCLUDE_CEF_CLIENT_H_ #pragma once +#include "include/cef_audio_handler.h" #include "include/cef_base.h" #include "include/cef_context_menu_handler.h" #include "include/cef_dialog_handler.h" @@ -60,6 +61,12 @@ /*--cef(source=client,no_debugct_check)--*/ class CefClient : public virtual CefBaseRefCounted { public: + /// + // Return the handler for audio rendering events. + /// + /*--cef()--*/ + virtual CefRefPtr GetAudioHandler() { return nullptr; } + /// // Return the handler for context menus. If no handler is provided the default // implementation will be used. diff --git a/include/internal/cef_types.h b/include/internal/cef_types.h index b86af1be4..a7c7dbb25 100644 --- a/include/internal/cef_types.h +++ b/include/internal/cef_types.h @@ -3003,6 +3003,137 @@ typedef struct _cef_composition_underline_t { cef_composition_underline_style_t style; } cef_composition_underline_t; +/// +// Enumerates the various representations of the ordering of audio channels. +// Must be kept synchronized with media::ChannelLayout from Chromium. +// See media\base\channel_layout.h +/// +typedef enum { + CEF_CHANNEL_LAYOUT_NONE = 0, + CEF_CHANNEL_LAYOUT_UNSUPPORTED = 1, + + // Front C + CEF_CHANNEL_LAYOUT_MONO = 2, + + // Front L, Front R + CEF_CHANNEL_LAYOUT_STEREO = 3, + + // Front L, Front R, Back C + CEF_CHANNEL_LAYOUT_2_1 = 4, + + // Front L, Front R, Front C + CEF_CHANNEL_LAYOUT_SURROUND = 5, + + // Front L, Front R, Front C, Back C + CEF_CHANNEL_LAYOUT_4_0 = 6, + + // Front L, Front R, Side L, Side R + CEF_CHANNEL_LAYOUT_2_2 = 7, + + // Front L, Front R, Back L, Back R + CEF_CHANNEL_LAYOUT_QUAD = 8, + + // Front L, Front R, Front C, Side L, Side R + CEF_CHANNEL_LAYOUT_5_0 = 9, + + // Front L, Front R, Front C, LFE, Side L, Side R + CEF_CHANNEL_LAYOUT_5_1 = 10, + + // Front L, Front R, Front C, Back L, Back R + CEF_CHANNEL_LAYOUT_5_0_BACK = 11, + + // Front L, Front R, Front C, LFE, Back L, Back R + CEF_CHANNEL_LAYOUT_5_1_BACK = 12, + + // Front L, Front R, Front C, Side L, Side R, Back L, Back R + CEF_CHANNEL_LAYOUT_7_0 = 13, + + // Front L, Front R, Front C, LFE, Side L, Side R, Back L, Back R + CEF_CHANNEL_LAYOUT_7_1 = 14, + + // Front L, Front R, Front C, LFE, Side L, Side R, Front LofC, Front RofC + CEF_CHANNEL_LAYOUT_7_1_WIDE = 15, + + // Stereo L, Stereo R + CEF_CHANNEL_LAYOUT_STEREO_DOWNMIX = 16, + + // Stereo L, Stereo R, LFE + CEF_CHANNEL_LAYOUT_2POINT1 = 17, + + // Stereo L, Stereo R, Front C, LFE + CEF_CHANNEL_LAYOUT_3_1 = 18, + + // Stereo L, Stereo R, Front C, Rear C, LFE + CEF_CHANNEL_LAYOUT_4_1 = 19, + + // Stereo L, Stereo R, Front C, Side L, Side R, Back C + CEF_CHANNEL_LAYOUT_6_0 = 20, + + // Stereo L, Stereo R, Side L, Side R, Front LofC, Front RofC + CEF_CHANNEL_LAYOUT_6_0_FRONT = 21, + + // Stereo L, Stereo R, Front C, Rear L, Rear R, Rear C + CEF_CHANNEL_LAYOUT_HEXAGONAL = 22, + + // Stereo L, Stereo R, Front C, LFE, Side L, Side R, Rear Center + CEF_CHANNEL_LAYOUT_6_1 = 23, + + // Stereo L, Stereo R, Front C, LFE, Back L, Back R, Rear Center + CEF_CHANNEL_LAYOUT_6_1_BACK = 24, + + // Stereo L, Stereo R, Side L, Side R, Front LofC, Front RofC, LFE + CEF_CHANNEL_LAYOUT_6_1_FRONT = 25, + + // Front L, Front R, Front C, Side L, Side R, Front LofC, Front RofC + CEF_CHANNEL_LAYOUT_7_0_FRONT = 26, + + // Front L, Front R, Front C, LFE, Back L, Back R, Front LofC, Front RofC + CEF_CHANNEL_LAYOUT_7_1_WIDE_BACK = 27, + + // Front L, Front R, Front C, Side L, Side R, Rear L, Back R, Back C. + CEF_CHANNEL_LAYOUT_OCTAGONAL = 28, + + // Channels are not explicitly mapped to speakers. + CEF_CHANNEL_LAYOUT_DISCRETE = 29, + + // Front L, Front R, Front C. Front C contains the keyboard mic audio. This + // layout is only intended for input for WebRTC. The Front C channel + // is stripped away in the WebRTC audio input pipeline and never seen outside + // of that. + CEF_CHANNEL_LAYOUT_STEREO_AND_KEYBOARD_MIC = 30, + + // Front L, Front R, Side L, Side R, LFE + CEF_CHANNEL_LAYOUT_4_1_QUAD_SIDE = 31, + + // Actual channel layout is specified in the bitstream and the actual channel + // count is unknown at Chromium media pipeline level (useful for audio + // pass-through mode). + CEF_CHANNEL_LAYOUT_BITSTREAM = 32, + + // Max value, must always equal the largest entry ever logged. + CEF_CHANNEL_LAYOUT_MAX = CEF_CHANNEL_LAYOUT_BITSTREAM +} cef_channel_layout_t; + +/// +// Structure representing the audio parameters for setting up the audio handler. +/// +typedef struct _cef_audio_parameters_t { + /// + // Layout of the audio channels + /// + cef_channel_layout_t channel_layout; + + /// + // Sample rate + // + int sample_rate; + + /// + // Number of frames per buffer + /// + int frames_per_buffer; +} cef_audio_parameters_t; + /// // Result codes for CefMediaRouter::CreateRoute. Should be kept in sync with // Chromium's media_router::RouteRequestResult::ResultCode type. diff --git a/include/internal/cef_types_wrappers.h b/include/internal/cef_types_wrappers.h index 7e07755c6..b7f0e3517 100644 --- a/include/internal/cef_types_wrappers.h +++ b/include/internal/cef_types_wrappers.h @@ -960,4 +960,23 @@ struct CefCompositionUnderlineTraits { /// typedef CefStructBase CefCompositionUnderline; +struct CefAudioParametersTraits { + typedef cef_audio_parameters_t struct_type; + + static inline void init(struct_type* s) {} + + static inline void clear(struct_type* s) {} + + static inline void set(const struct_type* src, + struct_type* target, + bool copy) { + *target = *src; + } +}; + +/// +// Class representing CefAudioParameters settings +/// +typedef CefStructBase CefAudioParameters; + #endif // CEF_INCLUDE_INTERNAL_CEF_TYPES_WRAPPERS_H_ diff --git a/libcef/browser/audio_capturer.cc b/libcef/browser/audio_capturer.cc new file mode 100644 index 000000000..3eed7a75a --- /dev/null +++ b/libcef/browser/audio_capturer.cc @@ -0,0 +1,125 @@ +// Copyright (c) 2019 The Chromium Embedded Framework Authors. +// Portions copyright (c) 2011 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 "libcef/browser/audio_capturer.h" +#include "libcef/browser/browser_host_impl.h" + +#include "components/mirroring/service/captured_audio_input.h" +#include "content/public/browser/audio_loopback_stream_creator.h" +#include "media/audio/audio_input_device.h" + +namespace { + +media::ChannelLayout TranslateChannelLayout( + cef_channel_layout_t channel_layout) { + // Verify that our enum matches Chromium's values. The enum values match + // between those enums and existing values don't ever change, so it's enough + // to check that there are no new ones added. + static_assert( + static_cast(CEF_CHANNEL_LAYOUT_MAX) == + static_cast(media::CHANNEL_LAYOUT_MAX), + "cef_channel_layout_t must match the ChannelLayout enum in Chromium"); + return static_cast(channel_layout); +} + +void StreamCreatorHelper( + content::WebContents* source_web_contents, + content::AudioLoopbackStreamCreator* audio_stream_creator, + mojo::PendingRemote client, + const media::AudioParameters& params, + uint32_t total_segments) { + audio_stream_creator->CreateLoopbackStream( + source_web_contents, params, total_segments, + base::BindRepeating( + [](mojo::PendingRemote + client, + mojo::PendingRemote stream, + mojo::PendingReceiver + client_receiver, + media::mojom::ReadOnlyAudioDataPipePtr data_pipe) { + mojo::Remote + audio_client(std::move(client)); + audio_client->StreamCreated( + std::move(stream), std::move(client_receiver), + std::move(data_pipe), false /* initially_muted */); + }, + base::Passed(&client))); +} + +} // namespace + +CefAudioCapturer::CefAudioCapturer(const CefAudioParameters& params, + CefRefPtr browser, + CefRefPtr audio_handler) + : params_(params), + browser_(browser), + audio_handler_(audio_handler), + audio_stream_creator_(content::AudioLoopbackStreamCreator:: + CreateInProcessAudioLoopbackStreamCreator()) { + media::AudioParameters audio_params( + media::AudioParameters::AUDIO_PCM_LINEAR, + TranslateChannelLayout(params.channel_layout), params.sample_rate, + params.frames_per_buffer); + + if (!audio_params.IsValid()) { + LOG(ERROR) << "Invalid audio parameters"; + return; + } + + DCHECK(browser_); + DCHECK(audio_handler_); + DCHECK(browser_->web_contents()); + + channels_ = audio_params.channels(); + audio_input_device_ = new media::AudioInputDevice( + std::make_unique(base::BindRepeating( + &StreamCreatorHelper, base::Unretained(browser_->web_contents()), + base::Unretained(audio_stream_creator_.get()))), + media::AudioInputDevice::kLoopback); + + audio_input_device_->Initialize(audio_params, this); + audio_input_device_->Start(); +} + +CefAudioCapturer::~CefAudioCapturer() { + StopStream(); +} + +void CefAudioCapturer::OnCaptureStarted() { + audio_handler_->OnAudioStreamStarted(browser_, params_, channels_); + DCHECK(!capturing_); + capturing_ = true; +} + +void CefAudioCapturer::Capture(const media::AudioBus* source, + base::TimeTicks audio_capture_time, + double /*volume*/, + bool /*key_pressed*/) { + const int channels = source->channels(); + std::array data; + DCHECK(channels == channels_); + DCHECK(channels <= static_cast(data.size())); + for (int c = 0; c < channels; ++c) { + data[c] = source->channel(c); + } + base::TimeDelta pts = audio_capture_time - base::TimeTicks::UnixEpoch(); + audio_handler_->OnAudioStreamPacket(browser_, data.data(), source->frames(), + pts.InMilliseconds()); +} + +void CefAudioCapturer::OnCaptureError(const std::string& message) { + audio_handler_->OnAudioStreamError(browser_, message); + StopStream(); +} + +void CefAudioCapturer::StopStream() { + if (audio_input_device_) + audio_input_device_->Stop(); + if (capturing_) + audio_handler_->OnAudioStreamStopped(browser_); + + audio_input_device_ = nullptr; + capturing_ = false; +} \ No newline at end of file diff --git a/libcef/browser/audio_capturer.h b/libcef/browser/audio_capturer.h new file mode 100644 index 000000000..98014814a --- /dev/null +++ b/libcef/browser/audio_capturer.h @@ -0,0 +1,53 @@ +// Copyright (c) 2019 The Chromium Embedded Framework Authors. +// Portions copyright (c) 2011 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 CEF_LIBCEF_BROWSER_AUDIO_CAPTURER_H_ +#define CEF_LIBCEF_BROWSER_AUDIO_CAPTURER_H_ +#pragma once + +#include "include/internal/cef_ptr.h" +#include "include/internal/cef_types_wrappers.h" + +#include "media/base/audio_capturer_source.h" + +namespace content { +class AudioLoopbackStreamCreator; +} // namespace content + +namespace media { +class AudioInputDevice; +} // namespace media + +class CefAudioHandler; +class CefBrowserHostImpl; + +class CefAudioCapturer : public media::AudioCapturerSource::CaptureCallback { + public: + CefAudioCapturer(const CefAudioParameters& params, + CefRefPtr browser, + CefRefPtr audio_handler); + ~CefAudioCapturer() override; + + private: + void OnCaptureStarted() override; + void Capture(const media::AudioBus* audio_source, + base::TimeTicks audio_capture_time, + double volume, + bool key_pressed) override; + void OnCaptureError(const std::string& message) override; + void OnCaptureMuted(bool is_muted) override {} + + void StopStream(); + + CefAudioParameters params_; + CefRefPtr browser_; + CefRefPtr audio_handler_; + std::unique_ptr audio_stream_creator_; + scoped_refptr audio_input_device_; + bool capturing_ = false; + int channels_ = 0; +}; + +#endif // CEF_LIBCEF_BROWSER_AUDIO_CAPTURER_H_ \ No newline at end of file diff --git a/libcef/browser/browser_host_impl.cc b/libcef/browser/browser_host_impl.cc index 24bdc5b46..5aa3013d9 100644 --- a/libcef/browser/browser_host_impl.cc +++ b/libcef/browser/browser_host_impl.cc @@ -8,6 +8,7 @@ #include #include +#include "libcef/browser/audio_capturer.h" #include "libcef/browser/browser_context.h" #include "libcef/browser/browser_info.h" #include "libcef/browser/browser_info_manager.h" @@ -194,6 +195,9 @@ void OnDownloadImage(uint32 max_image_size, image_impl.get()); } +static constexpr base::TimeDelta kRecentlyAudibleTimeout = + base::TimeDelta::FromSeconds(2); + } // namespace // CefBrowserHost static methods. @@ -1559,6 +1563,10 @@ void CefBrowserHostImpl::DestroyBrowser() { javascript_dialog_manager_.reset(nullptr); menu_manager_.reset(nullptr); + // Delete the audio capturer + recently_audible_timer_.Stop(); + audio_capturer_.reset(nullptr); + // Delete the platform delegate. platform_delegate_.reset(nullptr); @@ -2697,6 +2705,25 @@ void CefBrowserHostImpl::DidUpdateFaviconURL( } } +void CefBrowserHostImpl::OnAudioStateChanged(bool audible) { + if (audible) { + recently_audible_timer_.Stop(); + StartAudioCapturer(); + } else if (audio_capturer_) { + // If you have a media playing that has a short quiet moment, web_contents + // will immediately switch to non-audible state. We don't want to stop + // audio stream so quickly, let's give the stream some time to resume + // playing. + recently_audible_timer_.Start( + FROM_HERE, kRecentlyAudibleTimeout, + base::BindOnce(&CefBrowserHostImpl::OnRecentlyAudibleTimerFired, this)); + } +} + +void CefBrowserHostImpl::OnRecentlyAudibleTimerFired() { + audio_capturer_.reset(); +} + bool CefBrowserHostImpl::OnMessageReceived(const IPC::Message& message) { // Handle the cursor message here if mouse cursor change is disabled instead // of propegating the message to the normal handler. @@ -2792,6 +2819,25 @@ bool CefBrowserHostImpl::HasObserver(Observer* observer) const { return observers_.HasObserver(observer); } +void CefBrowserHostImpl::StartAudioCapturer() { + if (!client_.get() || audio_capturer_) + return; + + CefRefPtr audio_handler = client_->GetAudioHandler(); + if (!audio_handler.get()) + return; + + CefAudioParameters params; + params.channel_layout = CEF_CHANNEL_LAYOUT_STEREO; + params.sample_rate = media::AudioParameters::kAudioCDSampleRate; + params.frames_per_buffer = 1024; + + if (!audio_handler->GetAudioParameters(this, params)) + return; + + audio_capturer_.reset(new CefAudioCapturer(params, this, audio_handler)); +} + CefBrowserHostImpl::NavigationLock::NavigationLock( CefRefPtr browser) : browser_(browser) { diff --git a/libcef/browser/browser_host_impl.h b/libcef/browser/browser_host_impl.h index 2cc2e8005..bacfd33b3 100644 --- a/libcef/browser/browser_host_impl.h +++ b/libcef/browser/browser_host_impl.h @@ -48,6 +48,7 @@ class Widget; } #endif // defined(USE_AURA) +class CefAudioCapturer; class CefBrowserInfo; class CefBrowserPlatformDelegate; class CefDevToolsFrontend; @@ -486,6 +487,7 @@ class CefBrowserHostImpl : public CefBrowserHost, base::ProcessId plugin_pid) override; void DidUpdateFaviconURL( const std::vector& candidates) override; + void OnAudioStateChanged(bool audible) override; bool OnMessageReceived(const IPC::Message& message) override; bool OnMessageReceived(const IPC::Message& message, content::RenderFrameHost* render_frame_host) override; @@ -505,6 +507,7 @@ class CefBrowserHostImpl : public CefBrowserHost, void AddObserver(Observer* observer); void RemoveObserver(Observer* observer); bool HasObserver(Observer* observer) const; + class NavigationLock final { private: friend class CefBrowserHostImpl; @@ -585,6 +588,10 @@ class CefBrowserHostImpl : public CefBrowserHost, void ConfigureAutoResize(); + void StartAudioCapturer(); + + void OnRecentlyAudibleTimerFired(); + CefBrowserSettings settings_; CefRefPtr client_; scoped_refptr browser_info_; @@ -667,6 +674,14 @@ class CefBrowserHostImpl : public CefBrowserHost, CefRefPtr extension_; bool is_background_host_ = false; + // Used for capturing audio for CefAudioHandler. + std::unique_ptr audio_capturer_; + + // Timer for determining when "recently audible" transitions to false. This + // starts running when a tab stops being audible, and is canceled if it starts + // being audible again before it fires. + base::OneShotTimer recently_audible_timer_; + // Used with auto-resize. bool auto_resize_enabled_ = false; gfx::Size auto_resize_min_; diff --git a/libcef_dll/cpptoc/audio_handler_cpptoc.cc b/libcef_dll/cpptoc/audio_handler_cpptoc.cc new file mode 100644 index 000000000..fc8dc3f5c --- /dev/null +++ b/libcef_dll/cpptoc/audio_handler_cpptoc.cc @@ -0,0 +1,191 @@ +// Copyright (c) 2020 The Chromium Embedded Framework Authors. All rights +// reserved. Use of this source code is governed by a BSD-style license that +// can be found in the LICENSE file. +// +// --------------------------------------------------------------------------- +// +// This file was generated by the CEF translator tool. If making changes by +// hand only do so within the body of existing method and function +// implementations. See the translator.README.txt file in the tools directory +// for more information. +// +// $hash=4568fbb1f264fe9a900784aa3040c406a919ad37$ +// + +#include "libcef_dll/cpptoc/audio_handler_cpptoc.h" +#include "libcef_dll/ctocpp/browser_ctocpp.h" +#include "libcef_dll/shutdown_checker.h" + +namespace { + +// MEMBER FUNCTIONS - Body may be edited by hand. + +int CEF_CALLBACK +audio_handler_get_audio_parameters(struct _cef_audio_handler_t* self, + struct _cef_browser_t* browser, + cef_audio_parameters_t* params) { + shutdown_checker::AssertNotShutdown(); + + // AUTO-GENERATED CONTENT - DELETE THIS COMMENT BEFORE MODIFYING + + DCHECK(self); + if (!self) + return 0; + // Verify param: browser; type: refptr_diff + DCHECK(browser); + if (!browser) + return 0; + // Verify param: params; type: simple_byref + DCHECK(params); + if (!params) + return 0; + + // Translate param: params; type: simple_byref + CefAudioParameters paramsVal = params ? *params : CefAudioParameters(); + + // Execute + bool _retval = CefAudioHandlerCppToC::Get(self)->GetAudioParameters( + CefBrowserCToCpp::Wrap(browser), paramsVal); + + // Restore param: params; type: simple_byref + if (params) + *params = paramsVal; + + // Return type: bool + return _retval; +} + +void CEF_CALLBACK +audio_handler_on_audio_stream_started(struct _cef_audio_handler_t* self, + struct _cef_browser_t* browser, + const cef_audio_parameters_t* params, + int channels) { + shutdown_checker::AssertNotShutdown(); + + // AUTO-GENERATED CONTENT - DELETE THIS COMMENT BEFORE MODIFYING + + DCHECK(self); + if (!self) + return; + // Verify param: browser; type: refptr_diff + DCHECK(browser); + if (!browser) + return; + // Verify param: params; type: simple_byref_const + DCHECK(params); + if (!params) + return; + + // Translate param: params; type: simple_byref_const + CefAudioParameters paramsVal = params ? *params : CefAudioParameters(); + + // Execute + CefAudioHandlerCppToC::Get(self)->OnAudioStreamStarted( + CefBrowserCToCpp::Wrap(browser), paramsVal, channels); +} + +void CEF_CALLBACK +audio_handler_on_audio_stream_packet(struct _cef_audio_handler_t* self, + struct _cef_browser_t* browser, + const float** data, + int frames, + int64 pts) { + shutdown_checker::AssertNotShutdown(); + + // AUTO-GENERATED CONTENT - DELETE THIS COMMENT BEFORE MODIFYING + + DCHECK(self); + if (!self) + return; + // Verify param: browser; type: refptr_diff + DCHECK(browser); + if (!browser) + return; + // Verify param: data; type: simple_byaddr + DCHECK(data); + if (!data) + return; + + // Execute + CefAudioHandlerCppToC::Get(self)->OnAudioStreamPacket( + CefBrowserCToCpp::Wrap(browser), data, frames, pts); +} + +void CEF_CALLBACK +audio_handler_on_audio_stream_stopped(struct _cef_audio_handler_t* self, + struct _cef_browser_t* browser) { + shutdown_checker::AssertNotShutdown(); + + // AUTO-GENERATED CONTENT - DELETE THIS COMMENT BEFORE MODIFYING + + DCHECK(self); + if (!self) + return; + // Verify param: browser; type: refptr_diff + DCHECK(browser); + if (!browser) + return; + + // Execute + CefAudioHandlerCppToC::Get(self)->OnAudioStreamStopped( + CefBrowserCToCpp::Wrap(browser)); +} + +void CEF_CALLBACK +audio_handler_on_audio_stream_error(struct _cef_audio_handler_t* self, + struct _cef_browser_t* browser, + const cef_string_t* message) { + shutdown_checker::AssertNotShutdown(); + + // AUTO-GENERATED CONTENT - DELETE THIS COMMENT BEFORE MODIFYING + + DCHECK(self); + if (!self) + return; + // Verify param: browser; type: refptr_diff + DCHECK(browser); + if (!browser) + return; + // Verify param: message; type: string_byref_const + DCHECK(message); + if (!message) + return; + + // Execute + CefAudioHandlerCppToC::Get(self)->OnAudioStreamError( + CefBrowserCToCpp::Wrap(browser), CefString(message)); +} + +} // namespace + +// CONSTRUCTOR - Do not edit by hand. + +CefAudioHandlerCppToC::CefAudioHandlerCppToC() { + GetStruct()->get_audio_parameters = audio_handler_get_audio_parameters; + GetStruct()->on_audio_stream_started = audio_handler_on_audio_stream_started; + GetStruct()->on_audio_stream_packet = audio_handler_on_audio_stream_packet; + GetStruct()->on_audio_stream_stopped = audio_handler_on_audio_stream_stopped; + GetStruct()->on_audio_stream_error = audio_handler_on_audio_stream_error; +} + +// DESTRUCTOR - Do not edit by hand. + +CefAudioHandlerCppToC::~CefAudioHandlerCppToC() { + shutdown_checker::AssertNotShutdown(); +} + +template <> +CefRefPtr CefCppToCRefCounted< + CefAudioHandlerCppToC, + CefAudioHandler, + cef_audio_handler_t>::UnwrapDerived(CefWrapperType type, + cef_audio_handler_t* s) { + NOTREACHED() << "Unexpected class type: " << type; + return nullptr; +} + +template <> +CefWrapperType CefCppToCRefCounted::kWrapperType = + WT_AUDIO_HANDLER; diff --git a/libcef_dll/cpptoc/audio_handler_cpptoc.h b/libcef_dll/cpptoc/audio_handler_cpptoc.h new file mode 100644 index 000000000..b4778c8dc --- /dev/null +++ b/libcef_dll/cpptoc/audio_handler_cpptoc.h @@ -0,0 +1,37 @@ +// Copyright (c) 2020 The Chromium Embedded Framework Authors. All rights +// reserved. Use of this source code is governed by a BSD-style license that +// can be found in the LICENSE file. +// +// --------------------------------------------------------------------------- +// +// This file was generated by the CEF translator tool. If making changes by +// hand only do so within the body of existing method and function +// implementations. See the translator.README.txt file in the tools directory +// for more information. +// +// $hash=27689a3c353f267fb650ec5b7dc095e0a6be8b13$ +// + +#ifndef CEF_LIBCEF_DLL_CPPTOC_AUDIO_HANDLER_CPPTOC_H_ +#define CEF_LIBCEF_DLL_CPPTOC_AUDIO_HANDLER_CPPTOC_H_ +#pragma once + +#if !defined(WRAPPING_CEF_SHARED) +#error This file can be included wrapper-side only +#endif + +#include "include/capi/cef_audio_handler_capi.h" +#include "include/cef_audio_handler.h" +#include "libcef_dll/cpptoc/cpptoc_ref_counted.h" + +// Wrap a C++ class with a C structure. +// This class may be instantiated and accessed wrapper-side only. +class CefAudioHandlerCppToC : public CefCppToCRefCounted { + public: + CefAudioHandlerCppToC(); + virtual ~CefAudioHandlerCppToC(); +}; + +#endif // CEF_LIBCEF_DLL_CPPTOC_AUDIO_HANDLER_CPPTOC_H_ diff --git a/libcef_dll/cpptoc/client_cpptoc.cc b/libcef_dll/cpptoc/client_cpptoc.cc index 452401c36..9e54e6aef 100644 --- a/libcef_dll/cpptoc/client_cpptoc.cc +++ b/libcef_dll/cpptoc/client_cpptoc.cc @@ -9,10 +9,11 @@ // implementations. See the translator.README.txt file in the tools directory // for more information. // -// $hash=154a21a2f4ac985eeed2d28ad9479f322c4aad07$ +// $hash=04cee2c6a1910d7084c556f1bde99ba971b354d2$ // #include "libcef_dll/cpptoc/client_cpptoc.h" +#include "libcef_dll/cpptoc/audio_handler_cpptoc.h" #include "libcef_dll/cpptoc/context_menu_handler_cpptoc.h" #include "libcef_dll/cpptoc/dialog_handler_cpptoc.h" #include "libcef_dll/cpptoc/display_handler_cpptoc.h" @@ -34,6 +35,22 @@ namespace { // MEMBER FUNCTIONS - Body may be edited by hand. +cef_audio_handler_t* CEF_CALLBACK +client_get_audio_handler(struct _cef_client_t* self) { + // AUTO-GENERATED CONTENT - DELETE THIS COMMENT BEFORE MODIFYING + + DCHECK(self); + if (!self) + return NULL; + + // Execute + CefRefPtr _retval = + CefClientCppToC::Get(self)->GetAudioHandler(); + + // Return type: refptr_same + return CefAudioHandlerCppToC::Wrap(_retval); +} + struct _cef_context_menu_handler_t* CEF_CALLBACK client_get_context_menu_handler(struct _cef_client_t* self) { // AUTO-GENERATED CONTENT - DELETE THIS COMMENT BEFORE MODIFYING @@ -280,6 +297,7 @@ client_on_process_message_received(struct _cef_client_t* self, // CONSTRUCTOR - Do not edit by hand. CefClientCppToC::CefClientCppToC() { + GetStruct()->get_audio_handler = client_get_audio_handler; GetStruct()->get_context_menu_handler = client_get_context_menu_handler; GetStruct()->get_dialog_handler = client_get_dialog_handler; GetStruct()->get_display_handler = client_get_display_handler; diff --git a/libcef_dll/ctocpp/audio_handler_ctocpp.cc b/libcef_dll/ctocpp/audio_handler_ctocpp.cc new file mode 100644 index 000000000..74d23aaca --- /dev/null +++ b/libcef_dll/ctocpp/audio_handler_ctocpp.cc @@ -0,0 +1,164 @@ +// Copyright (c) 2020 The Chromium Embedded Framework Authors. All rights +// reserved. Use of this source code is governed by a BSD-style license that +// can be found in the LICENSE file. +// +// --------------------------------------------------------------------------- +// +// This file was generated by the CEF translator tool. If making changes by +// hand only do so within the body of existing method and function +// implementations. See the translator.README.txt file in the tools directory +// for more information. +// +// $hash=d7f2fb8ad6fe2bd0ab928b09ab596b12e9049ddf$ +// + +#include "libcef_dll/ctocpp/audio_handler_ctocpp.h" +#include "libcef_dll/cpptoc/browser_cpptoc.h" +#include "libcef_dll/shutdown_checker.h" + +// VIRTUAL METHODS - Body may be edited by hand. + +NO_SANITIZE("cfi-icall") +bool CefAudioHandlerCToCpp::GetAudioParameters(CefRefPtr browser, + CefAudioParameters& params) { + shutdown_checker::AssertNotShutdown(); + + cef_audio_handler_t* _struct = GetStruct(); + if (CEF_MEMBER_MISSING(_struct, get_audio_parameters)) + return false; + + // AUTO-GENERATED CONTENT - DELETE THIS COMMENT BEFORE MODIFYING + + // Verify param: browser; type: refptr_diff + DCHECK(browser.get()); + if (!browser.get()) + return false; + + // Execute + int _retval = _struct->get_audio_parameters( + _struct, CefBrowserCppToC::Wrap(browser), ¶ms); + + // Return type: bool + return _retval ? true : false; +} + +NO_SANITIZE("cfi-icall") +void CefAudioHandlerCToCpp::OnAudioStreamStarted( + CefRefPtr browser, + const CefAudioParameters& params, + int channels) { + shutdown_checker::AssertNotShutdown(); + + cef_audio_handler_t* _struct = GetStruct(); + if (CEF_MEMBER_MISSING(_struct, on_audio_stream_started)) + return; + + // AUTO-GENERATED CONTENT - DELETE THIS COMMENT BEFORE MODIFYING + + // Verify param: browser; type: refptr_diff + DCHECK(browser.get()); + if (!browser.get()) + return; + + // Execute + _struct->on_audio_stream_started(_struct, CefBrowserCppToC::Wrap(browser), + ¶ms, channels); +} + +NO_SANITIZE("cfi-icall") +void CefAudioHandlerCToCpp::OnAudioStreamPacket(CefRefPtr browser, + const float** data, + int frames, + int64 pts) { + shutdown_checker::AssertNotShutdown(); + + cef_audio_handler_t* _struct = GetStruct(); + if (CEF_MEMBER_MISSING(_struct, on_audio_stream_packet)) + return; + + // AUTO-GENERATED CONTENT - DELETE THIS COMMENT BEFORE MODIFYING + + // Verify param: browser; type: refptr_diff + DCHECK(browser.get()); + if (!browser.get()) + return; + // Verify param: data; type: simple_byaddr + DCHECK(data); + if (!data) + return; + + // Execute + _struct->on_audio_stream_packet(_struct, CefBrowserCppToC::Wrap(browser), + data, frames, pts); +} + +NO_SANITIZE("cfi-icall") +void CefAudioHandlerCToCpp::OnAudioStreamStopped( + CefRefPtr browser) { + shutdown_checker::AssertNotShutdown(); + + cef_audio_handler_t* _struct = GetStruct(); + if (CEF_MEMBER_MISSING(_struct, on_audio_stream_stopped)) + return; + + // AUTO-GENERATED CONTENT - DELETE THIS COMMENT BEFORE MODIFYING + + // Verify param: browser; type: refptr_diff + DCHECK(browser.get()); + if (!browser.get()) + return; + + // Execute + _struct->on_audio_stream_stopped(_struct, CefBrowserCppToC::Wrap(browser)); +} + +NO_SANITIZE("cfi-icall") +void CefAudioHandlerCToCpp::OnAudioStreamError(CefRefPtr browser, + const CefString& message) { + shutdown_checker::AssertNotShutdown(); + + cef_audio_handler_t* _struct = GetStruct(); + if (CEF_MEMBER_MISSING(_struct, on_audio_stream_error)) + return; + + // AUTO-GENERATED CONTENT - DELETE THIS COMMENT BEFORE MODIFYING + + // Verify param: browser; type: refptr_diff + DCHECK(browser.get()); + if (!browser.get()) + return; + // Verify param: message; type: string_byref_const + DCHECK(!message.empty()); + if (message.empty()) + return; + + // Execute + _struct->on_audio_stream_error(_struct, CefBrowserCppToC::Wrap(browser), + message.GetStruct()); +} + +// CONSTRUCTOR - Do not edit by hand. + +CefAudioHandlerCToCpp::CefAudioHandlerCToCpp() {} + +// DESTRUCTOR - Do not edit by hand. + +CefAudioHandlerCToCpp::~CefAudioHandlerCToCpp() { + shutdown_checker::AssertNotShutdown(); +} + +template <> +cef_audio_handler_t* +CefCToCppRefCounted::UnwrapDerived(CefWrapperType type, + CefAudioHandler* c) { + NOTREACHED() << "Unexpected class type: " << type; + return nullptr; +} + +template <> +CefWrapperType CefCToCppRefCounted::kWrapperType = + WT_AUDIO_HANDLER; diff --git a/libcef_dll/ctocpp/audio_handler_ctocpp.h b/libcef_dll/ctocpp/audio_handler_ctocpp.h new file mode 100644 index 000000000..16a0a7682 --- /dev/null +++ b/libcef_dll/ctocpp/audio_handler_ctocpp.h @@ -0,0 +1,51 @@ +// Copyright (c) 2020 The Chromium Embedded Framework Authors. All rights +// reserved. Use of this source code is governed by a BSD-style license that +// can be found in the LICENSE file. +// +// --------------------------------------------------------------------------- +// +// This file was generated by the CEF translator tool. If making changes by +// hand only do so within the body of existing method and function +// implementations. See the translator.README.txt file in the tools directory +// for more information. +// +// $hash=e5517ccac966337ef7dc576a24eedfe1154b2813$ +// + +#ifndef CEF_LIBCEF_DLL_CTOCPP_AUDIO_HANDLER_CTOCPP_H_ +#define CEF_LIBCEF_DLL_CTOCPP_AUDIO_HANDLER_CTOCPP_H_ +#pragma once + +#if !defined(BUILDING_CEF_SHARED) +#error This file can be included DLL-side only +#endif + +#include "include/capi/cef_audio_handler_capi.h" +#include "include/cef_audio_handler.h" +#include "libcef_dll/ctocpp/ctocpp_ref_counted.h" + +// Wrap a C structure with a C++ class. +// This class may be instantiated and accessed DLL-side only. +class CefAudioHandlerCToCpp : public CefCToCppRefCounted { + public: + CefAudioHandlerCToCpp(); + virtual ~CefAudioHandlerCToCpp(); + + // CefAudioHandler methods. + bool GetAudioParameters(CefRefPtr browser, + CefAudioParameters& params) override; + void OnAudioStreamStarted(CefRefPtr browser, + const CefAudioParameters& params, + int channels) override; + void OnAudioStreamPacket(CefRefPtr browser, + const float** data, + int frames, + int64 pts) override; + void OnAudioStreamStopped(CefRefPtr browser) override; + void OnAudioStreamError(CefRefPtr browser, + const CefString& message) override; +}; + +#endif // CEF_LIBCEF_DLL_CTOCPP_AUDIO_HANDLER_CTOCPP_H_ diff --git a/libcef_dll/ctocpp/client_ctocpp.cc b/libcef_dll/ctocpp/client_ctocpp.cc index fde52a23e..3176ebd30 100644 --- a/libcef_dll/ctocpp/client_ctocpp.cc +++ b/libcef_dll/ctocpp/client_ctocpp.cc @@ -9,13 +9,14 @@ // implementations. See the translator.README.txt file in the tools directory // for more information. // -// $hash=cb91642733be3dfd60a0d848c41c71bbe06a835c$ +// $hash=0e4556cf21b4d75aefbfa90963c6b5c9aba33bad$ // #include "libcef_dll/ctocpp/client_ctocpp.h" #include "libcef_dll/cpptoc/browser_cpptoc.h" #include "libcef_dll/cpptoc/frame_cpptoc.h" #include "libcef_dll/cpptoc/process_message_cpptoc.h" +#include "libcef_dll/ctocpp/audio_handler_ctocpp.h" #include "libcef_dll/ctocpp/context_menu_handler_ctocpp.h" #include "libcef_dll/ctocpp/dialog_handler_ctocpp.h" #include "libcef_dll/ctocpp/display_handler_ctocpp.h" @@ -32,6 +33,21 @@ // VIRTUAL METHODS - Body may be edited by hand. +NO_SANITIZE("cfi-icall") +CefRefPtr CefClientCToCpp::GetAudioHandler() { + cef_client_t* _struct = GetStruct(); + if (CEF_MEMBER_MISSING(_struct, get_audio_handler)) + return nullptr; + + // AUTO-GENERATED CONTENT - DELETE THIS COMMENT BEFORE MODIFYING + + // Execute + cef_audio_handler_t* _retval = _struct->get_audio_handler(_struct); + + // Return type: refptr_same + return CefAudioHandlerCToCpp::Wrap(_retval); +} + NO_SANITIZE("cfi-icall") CefRefPtr CefClientCToCpp::GetContextMenuHandler() { cef_client_t* _struct = GetStruct(); diff --git a/libcef_dll/ctocpp/client_ctocpp.h b/libcef_dll/ctocpp/client_ctocpp.h index fc976ced6..d42e9f2bd 100644 --- a/libcef_dll/ctocpp/client_ctocpp.h +++ b/libcef_dll/ctocpp/client_ctocpp.h @@ -9,7 +9,7 @@ // implementations. See the translator.README.txt file in the tools directory // for more information. // -// $hash=94a6eac6479bc8404b73ff49b581338ef198a0cc$ +// $hash=f751ee624cc3b8570cba8caa051c51bb7aeccaa7$ // #ifndef CEF_LIBCEF_DLL_CTOCPP_CLIENT_CTOCPP_H_ @@ -33,6 +33,7 @@ class CefClientCToCpp virtual ~CefClientCToCpp(); // CefClient methods. + CefRefPtr GetAudioHandler() override; CefRefPtr GetContextMenuHandler() override; CefRefPtr GetDialogHandler() override; CefRefPtr GetDisplayHandler() override; diff --git a/libcef_dll/wrapper_types.h b/libcef_dll/wrapper_types.h index fa468c83d..3e806f22a 100644 --- a/libcef_dll/wrapper_types.h +++ b/libcef_dll/wrapper_types.h @@ -9,7 +9,7 @@ // implementations. See the translator.README.txt file in the tools directory // for more information. // -// $hash=089392d929a9f7a3ca4fe7f53d63b98536505261$ +// $hash=9bfe176dfac4770800e95e2bbc0fafffbf0aeeaf$ // #ifndef CEF_LIBCEF_DLL_WRAPPER_TYPES_H_ @@ -21,6 +21,7 @@ enum CefWrapperType { WT_BASE_SCOPED, WT_ACCESSIBILITY_HANDLER, WT_APP, + WT_AUDIO_HANDLER, WT_AUTH_CALLBACK, WT_BEFORE_DOWNLOAD_CALLBACK, WT_BINARY_VALUE, diff --git a/tests/ceftests/audio_output_unittest.cc b/tests/ceftests/audio_output_unittest.cc new file mode 100644 index 000000000..8d134b6f7 --- /dev/null +++ b/tests/ceftests/audio_output_unittest.cc @@ -0,0 +1,1074 @@ +// Copyright (c) 2018 The Chromium Embedded Framework 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 "include/base/cef_bind.h" +#include "include/wrapper/cef_closure_task.h" +#include "tests/ceftests/test_handler.h" +#include "tests/gtest/include/gtest/gtest.h" +#include "tests/shared/browser/client_app_browser.h" + +using client::ClientAppBrowser; + +// Taken from: +// http://www.iandevlin.com/blog/2012/09/html5/html5-media-and-data-uri/ +#define AUDIO_DATA \ + "data:audio/" \ + "ogg;base64,T2dnUwACAAAAAAAAAAA+" \ + "HAAAAAAAAGyawCEBQGZpc2hlYWQAAwAAAAAAAAAAAAAA6AMAAAAAAAAAAAAAAAAAAOgDAAAAAA" \ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAABPZ2dTAAIAAAAAAAAAAINDAAAAAAAA9LkergEeAXZvcmJp" \ + "cwAAAAACRKwAAAAAAAAA7gIAAAAAALgBT2dnUwAAAAAAAAAAAAA+" \ + "HAAAAQAAAPvOJxcBUGZpc2JvbmUALAAAAINDAAADAAAARKwAAAAAAAABAAAAAAAAAAAAAAAAAA" \ + "AAAgAAAAAAAABDb250ZW50LVR5cGU6IGF1ZGlvL3ZvcmJpcw0KT2dnUwAAAAAAAAAAAACDQwAA" \ + "AQAAAGLSAC4Qdv//////////////////" \ + "cQN2b3JiaXMdAAAAWGlwaC5PcmcgbGliVm9yYmlzIEkgMjAwOTA3MDkCAAAAIwAAAEVOQ09ERV" \ + "I9ZmZtcGVnMnRoZW9yYS0wLjI2K3N2bjE2OTI0HgAAAFNPVVJDRV9PU0hBU0g9ODExM2FhYWI5" \ + "YzFiNjhhNwEFdm9yYmlzK0JDVgEACAAAADFMIMWA0JBVAAAQAABgJCkOk2ZJKaWUoSh5mJRISS" \ + "mllMUwiZiUicUYY4wxxhhjjDHGGGOMIDRkFQAABACAKAmOo+" \ + "ZJas45ZxgnjnKgOWlOOKcgB4pR4DkJwvUmY26mtKZrbs4pJQgNWQUAAAIAQEghhRRSSCGFFGKI" \ + "IYYYYoghhxxyyCGnnHIKKqigggoyyCCDTDLppJNOOumoo4466ii00EILLbTSSkwx1VZjrr0GXX" \ + "xzzjnnnHPOOeecc84JQkNWAQAgAAAEQgYZZBBCCCGFFFKIKaaYcgoyyIDQkFUAACAAgAAAAABH" \ + "kRRJsRTLsRzN0SRP8ixREzXRM0VTVE1VVVVVdV1XdmXXdnXXdn1ZmIVbuH1ZuIVb2IVd94VhGI" \ + "ZhGIZhGIZh+" \ + "H3f933f930gNGQVACABAKAjOZbjKaIiGqLiOaIDhIasAgBkAAAEACAJkiIpkqNJpmZqrmmbtmi" \ + "rtm3LsizLsgyEhqwCAAABAAQAAAAAAKBpmqZpmqZpmqZpmqZpmqZpmqZpmmZZlmVZlmVZlmVZl" \ + "mVZlmVZlmVZlmVZlmVZlmVZlmVZlmVZlmVZQGjIKgBAAgBAx3Ecx3EkRVIkx3IsBwgNWQUAyAA" \ + "ACABAUizFcjRHczTHczzHczxHdETJlEzN9EwPCA1ZBQAAAgAIAAAAAABAMRzFcRzJ0SRPUi3Tc" \ + "jVXcz3Xc03XdV1XVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVYHQkFUAAAQAACG" \ + "dZpZqgAgzkGEgNGQVAIAAAAAYoQhDDAgNWQUAAAQAAIih5CCa0JrzzTkOmuWgqRSb08GJVJsnu" \ + "amYm3POOeecbM4Z45xzzinKmcWgmdCac85JDJqloJnQmnPOeRKbB62p0ppzzhnnnA7GGWGcc85" \ + "p0poHqdlYm3POWdCa5qi5FJtzzomUmye1uVSbc84555xzzjnnnHPOqV6czsE54Zxzzonam2u5C" \ + "V2cc875ZJzuzQnhnHPOOeecc84555xzzglCQ1YBAEAAAARh2BjGnYIgfY4GYhQhpiGTHnSPDpO" \ + "gMcgppB6NjkZKqYNQUhknpXSC0JBVAAAgAACEEFJIIYUUUkghhRRSSCGGGGKIIaeccgoqqKSSi" \ + "irKKLPMMssss8wyy6zDzjrrsMMQQwwxtNJKLDXVVmONteaec645SGultdZaK6WUUkoppSA0ZBU" \ + "AAAIAQCBkkEEGGYUUUkghhphyyimnoIIKCA1ZBQAAAgAIAAAA8CTPER3RER3RER3RER3RER3P8" \ + "RxREiVREiXRMi1TMz1VVFVXdm1Zl3Xbt4Vd2HXf133f141fF4ZlWZZlWZZlWZZlWZZlWZZlCUJ" \ + "DVgEAIAAAAEIIIYQUUkghhZRijDHHnINOQgmB0JBVAAAgAIAAAAAAR3EUx5EcyZEkS7IkTdIsz" \ + "fI0T/M00RNFUTRNUxVd0RV10xZlUzZd0zVl01Vl1XZl2bZlW7d9WbZ93/d93/d93/d93/" \ + "d939d1IDRkFQAgAQCgIzmSIimSIjmO40iSBISGrAIAZAAABACgKI7iOI4jSZIkWZImeZZniZqp" \ + "mZ7pqaIKhIasAgAAAQAEAAAAAACgaIqnmIqniIrniI4oiZZpiZqquaJsyq7ruq7ruq7ruq7ruq" \ + "7ruq7ruq7ruq7ruq7ruq7ruq7ruq7rukBoyCoAQAIAQEdyJEdyJEVSJEVyJAcIDVkFAMgAAAgA" \ + "wDEcQ1Ikx7IsTfM0T/" \ + "M00RM90TM9VXRFFwgNWQUAAAIACAAAAAAAwJAMS7EczdEkUVIt1VI11VItVVQ9VVVVVVVVVVVV" \ + "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV1TRN0zSB0JCVAAAZAAACKcWahFCSQU5K7EVpxiAHrQ" \ + "blKYQYk9iL6ZhCyFFQKmQMGeRAydQxhhDzYmOnFELMi/" \ + "Glc4xBL8a4UkIowQhCQ1YEAFEAAAZJIkkkSfI0okj0JM0jijwRgCR6PI/" \ + "nSZ7I83geAEkUeR7Pk0SR5/" \ + "E8AQAAAQ4AAAEWQqEhKwKAOAEAiyR5HknyPJLkeTRNFCGKkqaJIs8zTZ5mikxTVaGqkqaJIs8z" \ + "TZonmkxTVaGqniiqKlV1XarpumTbtmHLniiqKlV1XabqumzZtiHbAAAAJE9TTZpmmjTNNImiak" \ + "JVJc0zVZpmmjTNNImiqUJVPVN0XabpukzTdbmuLEOWPdF0XaapukzTdbmuLEOWAQAASJ6nqjTN" \ + "NGmaaRJFU4VqSp6nqjTNNGmaaRJFVYWpeqbpukzTdZmm63JlWYYte6bpukzTdZmm65JdWYYsAw" \ + "AA0EzTlomi7BJF12WargvX1UxTtomiKxNF12WargvXFVXVlqmmLVNVWea6sgxZFlVVtpmqbFNV" \ + "Wea6sgxZBgAAAAAAAAAAgKiqtk1VZZlqyjLXlWXIsqiqtk1VZZmpyjLXtWXIsgAAgAEHAIAAE8" \ + "pAoSErAYAoAACH4liWpokix7EsTRNNjmNZmmaKJEnTPM80oVmeZ5rQNFFUVWiaKKoqAAACAAAK" \ + "HAAAAmzQlFgcoNCQlQBASACAw3EsS9M8z/" \ + "NEUTRNk+" \ + "NYlueJoiiapmmqKsexLM8TRVE0TdNUVZalaZ4niqJomqqqqtA0zxNFUTRNVVVVaJoomqZpqqqq" \ + "ui40TRRN0zRVVVVdF5rmeaJomqrquq4LPE8UTVNVXdd1AQAAAAAAAAAAAAAAAAAAAAAEAAAcOA" \ + "AABBhBJxlVFmGjCRcegEJDVgQAUQAAgDGIMcWYUQpCKSU0SkEJJZQKQmmppJRJSK211jIpqbXW" \ + "WiWltJZay6Ck1lprmYTWWmutAACwAwcAsAMLodCQlQBAHgAAgoxSjDnnHDVGKcacc44aoxRjzj" \ + "lHlVLKOecgpJQqxZxzDlJKGXPOOecopYw555xzlFLnnHPOOUqplM455xylVErnnHOOUiolY845" \ + "JwAAqMABACDARpHNCUaCCg1ZCQCkAgAYHMeyPM/" \ + "zTNE0LUnSNFEURdNUVUuSNE0UTVE1VZVlaZoomqaqui5N0zRRNE1VdV2q6nmmqaqu67pUV/" \ + "RMU1VdV5YBAAAAAAAAAAAAAQDgCQ4AQAU2rI5wUjQWWGjISgAgAwAAMQYhZAxCyBiEFEIIKaUQ" \ + "EgAAMOAAABBgQhkoNGQlAJAKAAAYo5RzzklJpUKIMecglNJShRBjzkEopaWoMcYglJJSa1FjjE" \ + "EoJaXWomshlJJSSq1F10IoJaXWWotSqlRKaq3FGKVUqZTWWosxSqlzSq3FGGOUUveUWoux1iil" \ + "dDLGGGOtzTnnZIwxxloLAEBocAAAO7BhdYSTorHAQkNWAgB5AAAIQkoxxhhjECGlGGPMMYeQUo" \ + "wxxhhUijHGHGMOQsgYY4wxByFkjDHnnIMQMsYYY85BCJ1zjjHnIITQOceYcxBC55xjzDkIoXOM" \ + "MeacAACgAgcAgAAbRTYnGAkqNGQlABAOAAAYw5hzjDkGnYQKIecgdA5CKqlUCDkHoXMQSkmpeA" \ + "46KSGUUkoqxXMQSgmhlJRaKy6GUkoopaTUUpExhFJKKSWl1ooxpoSQUkqptVaMMaGEVFJKKbZi" \ + "jI2lpNRaa60VY2wsJZXWWmutGGOMaym1FmOsxRhjXEuppRhrLMYY43tqLcZYYzHGGJ9baimmXA" \ + "sAMHlwAIBKsHGGlaSzwtHgQkNWAgC5AQAIQkoxxphjzjnnnHPOSaUYc8455yCEEEIIIZRKMeac" \ + "c85BByGEEEIoGXPOOQchhBBCCCGEUFLqmHMOQgghhBBCCCGl1DnnIIQQQgghhBBCSqlzzkEIIY" \ + "QQQgghhJRSCCGEEEIIIYQQQggppZRCCCGEEEIIIZQSUkophRBCCCWEEkoIJaSUUgohhBBCKaWE" \ + "UkJJKaUUQgillFBKKaGUkFJKKaUQQiillFBKKSWllFJKJZRSSikllFBKSimllEoooZRQSimllJ" \ + "RSSimVUkopJZRSSgkppZRSSqmUUkoppZRSUkoppZRSKaWUUkoppaSUUkoppVJKKaWUEkpJKaWU" \ + "UkqllFBKKaWUUlJKKaWUSgqllFJKKaUAAKADBwCAACMqLcROM648AkcUMkxAhYasBABSAQAAQi" \ + "illFJKKTWMUUoppZRSihyklFJKKaWUUkoppZRSSimVUkoppZRSSimllFJKKaWUUkoppZRSSiml" \ + "lFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKAcDdFw6APh" \ + "M2rI5wUjQWWGjISgAgFQAAMIYxxphyzjmllHPOOQadlEgp5yB0TkopPYQQQgidhJR6ByGEEEIp" \ + "KfUYQyghlJRS67GGTjoIpbTUaw8hhJRaaqn3HjKoKKWSUu89tVBSainG3ntLJbPSWmu9595LKi" \ + "nG2nrvObeSUkwtFgBgEuEAgLhgw+" \ + "oIJ0VjgYWGrAIAYgAACEMMQkgppZRSSinGGGOMMcYYY4wxxhhjjDHGGGOMMQEAgAkOAAABVrAr" \ + "s7Rqo7ipk7zog8AndMRmZMilVMzkRNAjNdRiJdihFdzgBWChISsBADIAAMRRrDXGXitiGISSai" \ + "wNQYxBibllxijlJObWKaWUk1hTyJRSzFmKJXRMKUYpphJCxpSkGGOMKXTSWs49t1RKCwAAgCAA" \ + "wECEzAQCBVBgIAMADhASpACAwgJDx3AREJBLyCgwKBwTzkmnDQBAECIzRCJiMUhMqAaKiukAYH" \ + "GBIR8AMjQ20i4uoMsAF3Rx14EQghCEIBYHUEACDk644Yk3POEGJ+" \ + "gUlToQAAAAAAAIAHgAAEg2gIhoZuY4Ojw+" \ + "QEJERkhKTE5QUlQEAAAAAAAQAD4AAJIVICKamTmODo8PkBCREZISkxOUFJUAAEAAAQAAAAAQQA" \ + "ACAgIAAAAAAAEAAAACAk9nZ1MABAAAAAAAAAAAPhwAAAIAAADItsciAQBPZ2dTAABAKgAAAAAA" \ + "AINDAAACAAAAi/k29xgB/4b/av9h/0j/Wv9g/1r/UP9l/1//" \ + "Wv8A2jWsrb6NXUc1CJ0sSdewtPbGlo1NaJI8UVTVUGRZipC555WVlSnnZVlZWVlZljm1c+" \ + "zimE1lYRMrAAAAAEGChIyc4DjOGcNecpzj3e5eskWraU5OsZ1ma2tra+/" \ + "QoUNbkyPMXUZO1Skw1yh8+fLly+84juMURSFhhhyPx1EDqAmLBR0xchzH8XgcYYYknU5HIoc//" \ + "F1uAOa6rplb8brWAjo6AuBaCWnBu1yRw+9I6HTe9bomx3Ecj8eHR7jhpx2EJSwhwxKWDtHpSA/" \ + "hd+Q6Q/" \ + "XNZeIut1JxXdd1FzPAbGI6kyYm6HQcNmEJi07r6Ojo6OjQ6XQdhMWCTscBwEd2xARjIprIJiYm" \ + "JiYyf2KCACDkOB6Px+O3AKDQkNscN32A7tIn3tm+wPdQiK1gI2FpTbSPWkfP39+nb29vT9+3/" \ + "Y+8NdEAfA+OmQ6zRtfR0dHR8ahTR0fH4+PjY0dHx2ynx8dHgB8U/" \ + "i6fLaUnx1wT25MmJiYmJqYDACDTYdbodB2EQ+9aRwD+Nkw+" \ + "hfQxSPHBdvQ2TD5FpJFBCCtwtLsEMYc15nbtXNNdkgqHYiKRlIwAAABAlCZiYkIIiThNSRKhE8" \ + "KqUrnsJ2hxoZt4CRurX076XaZaxJetiVOHTp0a+PgINiJWq8VwfLk+" \ + "cITkeOQ14Y4rvOkFV5gNbxGwcVJTDea6zsoAASCExwDXWK1chON6pdVirqN3roR6RupwgcQ1uT" \ + "LXI+" \ + "HyOoth7KQkYR7fAFOJv3TclGuuX2CS60rmmwgoZRIFU8icwlwDSea3MKrOGxMM1XtqaLgmDcCL" \ + "YEbscM8PuoIEXYE9Qj08y62k5aQRDimNrAslDCa0CL3XGSYaTW0Q2etDMZyiS435NgHG4HACkQ" \ + "xzYNnYqtvRwqPLDKAT1fRDd5KIJ45cOoeyA1FHC455K8BYpAAAZ2gMqDAOQPcz9/" \ + "v3uTNAASBXhW/" \ + "+wqevLAUrnjUnS7YzOs8s+" \ + "bpwXYrKdoXXGjBgp10SlQ8A3jb0scTwUeAFrmtD70uMfSS4gJeZlUhIlNsKco2uXVeY2VWl6JR" \ + "DSAhW4jYAQCYAAJCXD9bEGgGxF1Oz2UgEAhOlC0q5pjzL3fxjlQcAAACAjx8bmMEYnbAb1U4nz" \ + "BE2MsOHLwGuHz8oUi2qnhqYoTAuZWUNo0sfSn2HJJcA1xVleDATYEDmjGsqfYuV1VW3dhdQ11Y" \ + "rko0xrJHM6qZIpxW2qPLKAiBzakFdDasdLWtAzpUaGbaUXhZzReGzLuS71zqMZIhap91418WyG" \ + "4stA5xvC5AWfdPC0KFnhug9EJ6h0yAGfs54rQNMjP2JYPT0RkeosWCnkZ1GGYvGLCMRrhdEj8C" \ + "h8OOUvsYFzIyKCO+" \ + "MNsuxBAyvGFnp9QwEhcblgg5xA7gRNLmyHjMwEAWu57SEt2AIXIbqDRCqICCh7QEAvBIAAACSr" \ + "VYG45afyShwcSuuNzIo4AUK/" \ + "1ZvfgUABf42jCVGeVhwqQpxQ99SBB86rGrhPqKsDImUIoPYFTmNXd0Vlks7U8FRjYAEAACYOUp" \ + "Uk8RSEAkCIWK0JOXukmSu2R1+iWGIWLBM+mt0Up2tqni9YR6/" \ + "b6aK70i+" \ + "IV0EAzUMs4ZAYRQJwvNSInBWKJtFAgS9MiFgEEYDmIOZMK8E4h5xAegwErEGYWbSzKJ0E5mz+" \ + "AozI2QYjutAsEbhzsrxtoHkIjxIZo4Ho/" \ + "RpMMDTvsug986rhaceoIQiQucUUCJPKaOJwDKa0Y2kiRhjDxOG6EGJEyEhATLXC0j4qKckgkeE" \ + "ugjRA8B1MY8D3sIBr0kOVsvFwQTfLbj3ABCIMrHSLyQ4qbOdjCEK8gghdCNG3wyjgskAAICiE8" \ + "D3VkkAYPyxpQAAwPXlW2HA/I/NAfx2KQA+3q4MBYDE/" \ + "X+cAACyJJGwfpZxAP42TMkH+" \ + "0AOLxIzb8MUfZQRHR7AkSHICOeGMHdG55ULc3qMjEjSBwO0SAEAAAgZYhCWUSyoKAVKKKsQL5v" \ + "0RJaFKF0iIp4A4u42AYA9HEIPhlrCoWNiOrlxU6OmOcVsXyAWNWWyYvEg1fLKMHi1MRcAqZ6qs" \ + "KKPcJIoAfWgjkIjWXkBzZLBQ2X0djWBHvsi6aIQ6rQmZ50vcrgEuGNleEwUBA1WpKJiZkbhShj" \ + "a5TrjZ8uHdL4p6sJpn0748t/" \ + "4Ky401MB4FwAw6vRWc8BMAjnySEYJoc5+" \ + "VpmHtCG9622e9msJozQgHQ18GB16oycX3odS6siozeCNd2g8ow/" \ + "jDOloAgg3GK9JhkfU4FAwCLci2kD8KNGqMLrinHR6yujhyHcCArjgYYwpicBMtEILRJRmAK8nc" \ + "HC0MBHPNnh8fASAue68jrwrIuG/vZnupwGA/" \ + "v5t2CyABGSAA942LF5E8x+" \ + "NWB4kPtqGxYto9scgpxdJx11V1ABBREREEsTMk6t0u1QyNV3MRCIdAgAA4C6ZGSzQBMyUmAhNp" \ + "RgtYEkrCYlUtqSPpYbpbf2LmAYxxcEodhZD1DDVNE1z4VabMg0MUzksaBW5vkWD45q5luFKQ4x" \ + "Dl6XhA0w4GGKQyRZwjQbX8Y1Q1y10x21clDHA4EPjADidlLiWSmXCUjIzk18yZHjkFeGU5hOws" \ + "jSXKma1g2NpIJmVsRIyHQb04fcMMljQLC4eVrUpAsBbR2P80rIFh7xkaD+" \ + "qQRJAhCF6amxRZzQLYCgAwGHk4tTxhp46UZ1GR01UxokEw3RgDR8aYLLBhBVWdRfXkdlKNjRIn" \ + "tIN5WpsAhOGYW3WNE6ychBocusS6He+3SoISm3RZ/" \ + "ity3SkcDrh1O3GnqUWeMII2FsBAACOXK+ZAayOaQAAAMDr9fgQAAAQpgyQURIsAD43bNUE+" \ + "YGaVgonZW7YqgtmBDX1FCdVPNoASzrC7pwh6GrXnSTOhZliQkJITZQjAQAAEBayiEAASkSEEhU" \ + "nAlqMIl4iEBUvFwr9KaKiwRKlIixCWBS0QJTtFQwxraBqER3lw4O5wsxcLRbgeBwXM5WRLq6EX" \ + "AII0SuonlAqIjsEkPAiYT5wvA6YhOuYQ5AkIsK4nA7DuAjJhDkrtwWLUBWrcSQhFlOglSQ3XXB" \ + "N6sIjyXFczCTkSo5X5hpGyJJ7MtDTHSJLkPBbpMBy2F1Er3PU7qKM5yMsFhDGeEooCKVk4zwFE" \ + "9pNnPoeCBgAYsJ9JsGHiYDTHHEpzKldb3scr1fvvJRLdLrQPhYwq7FljfBg/" \ + "R6eNRZEBhQGYVX9QEdsDeG6qA6OAGjMBADgO6AIdQEAlaUfAAAAEM/" \ + "wQZz6AQAoK06aAAAAAGTherz+MgAAAMAHlAX1Uy0qAL42LNpFsx8HpWdH0LVh0T4i/" \ + "zhIHWskPTKhHTrYuWpKV5KkjEUnpIYIJAEAAIhAIBSI0aIlSlQoChGhUKQQioEWUqJCUYeEmB9" \ + "8T5k2E/" \ + "+DL3t1UQNjJmHEFP+" \ + "mrQyEabFWhEmubBFIrmAMZaYgjrBwHIfFMHkdTzDRuXSQqUP1KAKoAGJv6IC8ddTpGcyxIIyMN" \ + "kIHYlVoMSLCOpcBXBAKSR4Z5iJSHo/" \ + "h+sSn1w1zcL2+t+" \ + "vggs6EoRb1YfQRvRtEfcwboET1ohMO0JBOF4wuclcSqndFBhgiPPWeAT6CUAZAZwylLtL7MC6Y" \ + "NRgRVn1YnQwIRYTuUl3EIdFbu47ru3l9uMJcud4JcxHQ6RSD7hqATRcNGv0Q6EIJPAUwANBRKP" \ + "xWAYAIcjoAAADwMf0lHqCej/" \ + "n7+" \ + "9sAAAsWFtSVSDhHPrjcDBlFPWb95RQZfjds3cfIH5Ae5KTOd8PeYkz2Q8BDwXYfeeK0w1i5SQT" \ + "t1sByrhw9LiVZHOVBEgEAAKDECjFxMWamy8tFabpcQBMlLIwymhKDW/" \ + "motIQ8H8rNy3MJzBhFOsIQ0gAhTIG23swQ9rJa+" \ + "Vo1kgrfbpgarwknpUFFUXhkAZnUyFzo9nYLTpTQKNarPtd46sfSmS7CSgl1VkzAxOFWowvyqwi" \ + "yQM9Io9rMVtMNIw0N6duZoR3zWRYFWWQhVKbjuF7WnVTZYY8CY4e4jCT93sfJJACJBRpY1l6NS" \ + "QDEmmzDBPqhSdNmLPURTE4SPg9PiIuMSJ5SGvOh8fQAYFSRR2T4XYMZETqDnihEsuKtxoSvzEQ" \ + "OhhXKl2oCK1LfMJe11t/NvT/" \ + "EuCdvlaj1JXsG+" \ + "sLnIEHNJtDRjXHAGhudngIATJQBHPC5X0dy8cgAwAAAnP3s8A77BoAGigofEvUvtSQzzckL3lz" \ + "5SgA+N2xVxqYfiG2hQDRxw5p0TP7RUFYed2WZkTJkEIIMG+" \ + "ax2rmxQ086qaIcOihPIgEAQAgBQniAcIAZEPPwclRAKFomKe7hVnULf3/" \ + "PrQ43ubI48InJ1po2TLadA0rEUDVUsVMxwBjVmBC0w250lKGkDmEFeevFhBnCXCZDGDrECQYkg" \ + "QebcT2OKzNKQDKHqgnDSkN6oiMCsInFdb1GjTwpqw4J2QmJpC4LKoDrLwrHbc/" \ + "zI95E+Ki0iHd+wOuhI5EXB7E4UwUkRwKwRFLVul6uHiATCy54aCgNFdC7/" \ + "pjGcUyYMJw4OrgMxutRSL3lRBFhhNE7tAhCI0BJWCOb1aJpVEbyCMmnhOosy5XrmptCmDLAOhf" \ + "11KO/3ahqxOhRJZwP1/" \ + "AEZkTAcQBMVwwAAu8E9KzWgEiZCJPhBQAAMJcCAHp1D6QSAAAAzKfbAVCHthYLAAAAlQI8macA" \ + "IFQ+AD43bFXEXvzAuvFRN2xdx24/" \ + "FOvOfNzVZYRMMqQMOWHNBpJ27Ry92YXHdDyJgHJYAAAAPxIzmSBiki6amEBSVBYqBTFapoRCod" \ + "BcQooSE4rQEqQLIYHXj79ceaxwo/D71/" \ + "JVcaMMtwDAHFyFJLmS6+" \ + "hmgeso9zEQShTggBx3gco4QjLABeTBMDMltQ8Brgk7g1kxLtIOwrwCEwhsUeMKC7rG5ArX5HgN" \ + "fDrOIsfjlFavcc2EnBx3IoaOFs0aHKEuK26Jo1oGKpcKCVPHpO3dn7yJ0X8xXC73Lnd26CXLjg" \ + "MtCjN5hCFThAQ2oSFtOqOVGSDuE32hhYk4jsTlovpQozGwRbDa4UortUhDwkUAGGOR6jFYw2wX" \ + "FJWsYBDy4MgWAXCbVTMEAABQEJ9LgCwH3xYAAIBMa6ZNaQCgAcBTJ/" \ + "x3AOj3+wDAcLwer2MAAAAy2ACgwXf8bXKBCjY2xMkkMx+KLi9G1SRfQ+" \ + "pkrPnA2nUmeyDHu80EQVKEELbtjomYPTNESOfauaokPSZJA0UIkQAAYM42+guCLGQ+ns1y+" \ + "KgSupXRApdXJEpi4pSLDJJKRcndw82tg0wkAgPrVMAMn+iQ3W/huqg/" \ + "Dn51VgoLfPn9gVTIoESUeFyxOCk/hSsDF5NZW70MrmNVrOoWZlo5bmhxTDLHOHTC3u/" \ + "ymKjiOlaAEIzU6EcUCYPLmDnmVK1esZjWOPXKWdXtVr01YJGhu4TiVoJMBjheB9dxCXLovTEEh" \ + "RLGaUp2Y+m+" \ + "rAscdQDonFbotozWUeP0Lop2ZioVzASyLqcOHV0lOE9nZ1MAAUBTAAAAAAAAg0MAAAMAAABNPA" \ + "GzHHBCSXWA/4hFeXd4eHD/Tf9a/0z/RP9T/1T/" \ + "VP80TOZAm1Nx5OLKuqg4gORtwnDFBzFFjQzs0XZdrDdKYSDmSjid4LQfDhKi5+" \ + "h240ACA0CNW3cUDABUawBGbEDXTQBrAQCVA+gxJAeAooIVRYmfpDQ/" \ + "ADDPAJiZPJ4CKL0VwmKNDgAAABB4wcksgAwAnP100SFYKhzvrKcPFkH6ob/" \ + "mUaXIRDBTcgHEGFi5xJcO3laj2nwCDrhVGxPvuosRTqeLwWlAfYSrIzNoeauADicEfPVoAlDqQ" \ + "aRFq+" \ + "snciCkfCHS4qa8or0MBDMmF1j6cY1Cm4iGhjfGaeO3aieOGe5NDbGwDjMMcczOSPXRFkE9H8+" \ + "2tivg+AwTAGQNEQxiLnIquGjWEMUgZgkKXsxZc5FSaqfd3Uw1z81sIB8+X5WJ4h5VVZ9m4+" \ + "6i1buron8SP34vySmxQ7qLMCG0QEz0/" \ + "dT7XwCo2VZgmg6XM5ywWqdMz5QJ8fsMtqXl9ASTOGbvISIWw86dzoFrOPK5YYp0AJQNE0WyW7g" \ + "QLT5gX0YMCEhLFyIFZx7T9tdYUcZB0VtJu6JumJQl5fBOSLhW37S5VXs34jK1Jk6VT/" \ + "x83HsZZHFaDLQov1dP9gNAMvmItMzF99N26+QM0xzGyd1NG3vx7cvGMQe2Flvf/" \ + "nbSajF9W32brvVMk+7b9uwetObQFh8AUjYUlVb4B2Kq5oLCGBuKhIP8byZy6k0Fy/" \ + "P7VLTPiCKAqKtn1I4yy4MRVYUi94hYM9q163TiXMVijoCQBlgBIAEAAGyfygLXGCzgsKJUvzv9" \ + "XsNFCjlU+yZdS4D2WD4dm8oyL0+5TdC9AAAAAOSQ49//" \ + "kCTDIgMwAI6dPc3tnLUcLWWKMTh3gAtGuRBrX8HcVR2OmbiORPgtuEJZSZiw4OIuJlOhrDgCWN" \ + "qTvazq8R1kGqXJdTJxtXJKFMGgpws9wcyEk9FRCbfVYK7HpfDKt6iRl2yPW3hTVjWw8HljZcsx" \ + "BrKd+rDCSQCAGKtxnRWOVybT4lpXTXidEUdPVgF4UL9swRV23u0ZD+" \ + "F0MRKXIawEFIB1ekIgDYGbwsMKU+" \ + "0ozKo9Z2TkC0c3gMxmkYQbJsxQOzYjDyQkIuv7bsFeisuWiRwzYCSRqHXOjQg98QZbtYEYxjGc" \ + "QcARkID2pQDgwPm9ngRunDrVrqxYyYraKzL4/" \ + "nJO3t3aACCBuWOB5ax1sNREyTiHA2wFfRZkaS4gkWMr6LMg2DOhYKapgsoCSDNCBtw9OxjKFWD" \ + "CykgduoxFYqpga3WXZEJdsKgHsYBJTARBmHkdFKsAQCmnXnwROSE7Hv6llTQVfDiNLyIjZBf2/" \ + "ksraS74cBrfqkTCRD5PUtVGNNMKXBQCXNlcVlBc3JeFFaQQFSmXY1K0woOGWAXocnERodAlpA6" \ + "qrJyaaTkB4Jizva1jptWWCYvjdk79BZjTT5tmqsXOxhCQCDJiPlKf74sLJSRcEWkl5I7tv9mpY" \ + "BrTuCLSSsgd27/" \ + "ZqWCapv0HIOTKyCpCDKk6j0huPhQVAAtocQIRWkiGIAYtJsnw9BAjYl4xuuSLUnHcx2tjuUQyZ" \ + "hTkDACHjsQhFltHamcRO8SwkekNAZhRrXYOp5uxrLd4XtEwbb8z73zWNpwRWZbbM239xM8kh+" \ + "uMyLLcnj0cip8LZsytqkYyyYqoriqQu5FSj1dkFaJAQSnL43E4lBKlTCgqQknRouwQXhEVEbg7" \ + "NHGEdOWrR9X0wRWbhOXu4rUZu3p+" \ + "8FtsDIsThmlnNTCMKQ4thuOOKKuNZUbFxBOPz3vyLzwRKSzpbvMJu4IBp56ICCKJx9zGrmDDqb" \ + "OGUiFCT7KTkUeojozcJkSBhIijQiLK54mJE4GIKAsFkCwT8yUwIelyMU2EIAJRP8Vo0dPebkqG" \ + "aBR4ylMQA3+cdMJiYz7r7wm1Mez8OGaorRXUNFVncCyO9y+tTnwJTwwRNQtG5BcKZh5BQ70FA/" \ + "IHBTPXWRUpUnQexLSqkClyVlENHAqLYQOXD15ITCgkhBDxMhE3DxaKif+" \ + "YkFkgIqZ4SgmQMW2qv3wdmTb2rsVGrGpx6LhDJ2wcc4TF3saBL6Gd+vhJLbZdxG7+Z25+" \ + "jQzaNYQuBh1BDYutlYCuIXQx6Aiq1aqVkFstWUNWFBEioowYI7tudxub1uSo3FXFSbHERiQSAg" \ + "IAAJAymXQJKSDADKEYiBhdQYsKPMVBM0UkxcSYEqFEpJDaskhhOmY6sBhiqCm2YmIRU7Cghi9b" \ + "QQwceq3qcWAxCjlmZiH6YF0HALHFRchc/" \ + "IWBhRpjMYxFxrA4wLQKomKVxEB1ozEEOBlBjDOceDHHXZlryEFrZOCVme9uTSOkpQbDgxAsBoQ" \ + "6dBjBwWrXj4MpbNqgByzqAzrwgIMhAGSRUAI4ImPoj+4YhtEJ450rTr3Te+" \ + "oKQ0EdEh0AuLwvoUgAWPT6ibwjn2ZyAcMQETA6C9hhQkdsorFi0RUGeqCkDB1Dd4A28ScCMQJw" \ + "uxH6QfQ/" \ + "yMCNHCVggaJ1aR0BADDzeh0BwEQEwFyvuyQAAODD7zgA4LJkAL4G9D5GmqVR02oJnHPAa0DvY6" \ + "RZGjUtlsA5B3Rn1hAyRSpTRUo7mDuT66pYUiZ62NwCAxIAAACNtZcoBlgINkBEZhUtSYASZ1pI" \ + "CE0IAehCyA7TQgCCvQOL2OG46YRjoqO964AUEUVtMdWiMpMcF3OEHHwIyRNOXSSFFzOi1+" \ + "tdFlz6MJEIiHfoFEGsBWqK2llRk+NFMszjmrkOYygQDQIEGAA+" \ + "HOSYuSZcnIoDMgcTuGgKb4MccORxwXUwMNM5NAQ+" \ + "jEtmJnk8GAgTGCOIAaGRvV5HqNPpDSamPgLUZYEEhXR53c0QJzo6dJE7ZC1BLTrDgLgQUV0Y1Z" \ + "PI8bg60YUL6iMCM8fjQ0JCwjg9wzJDSCeIN5aeRgETm7ShW7pNB2jdSQAA1YwA0I0Y3ZIcAgAA" \ + "76QMFAAnAgAAoe4uy6WEMgFavwv68IdhABANMmTIIwAAAAA2XwkkAD43TD0Fe0BLqzkJe6wblp" \ + "6CPaCl1ZSEPR4R0mKQdrfEUNXOo1NjxXQ2M5EiggQAAKgQTLsoYbmoUISmWBTJoFKcEitjFhGy" \ + "QCgmQigBDQjFBZ4QZYPjTsiOMNVioDY+dqjHBzjITDKZHBoZrnA6iiCYOUJgwqFq+" \ + "mDEu24kxORIYLjm+" \ + "PR65PUKYWASXsmhngAEHkhMCHx4ZAgAmbmSkb5JI4ZhgHh2YtpgAvN4XMOQmTMRjA6aHkCH+" \ + "jA6FVDEAWqKgjhHlD9ZS4bOogVPLEZQBj2SnvQEZxim+" \ + "mvvsK7dgCUswSOc9JLIBK6oCaPbNTkHBedS9YS4cPG65rQAudgwO6bHoKsJYTTAMMK4AANmHVx" \ + "jTJ7Q6QEdEKZ7kpuib8EQLZAqQIkrdnQAGCnS4ojg7a1FusboazG0MBFHAGiOI6/" \ + "H2pEcTw7goALeNkzFJ3EoxLSYAkePbcNUfBKHQk6LKVD0eNdQEYqElJIghn03CFyXqw6RHi66w" \ + "hQTcEMAAADsJRmSWJKEhBABRQi7i3sIRDxERcFEIE6J00wooiZWgLI6sqqoaaImA+" \ + "AdUsJsHFq5eAwXHDQG1CCPzECqbQSSuXI9EjRqkAnfqWtrj2u6Y38kvY8UZgIRAAA9gwTCcT0O" \ + "QgBi8JqZ4zokjJNENj7UEZiDVyYzBMiQObjpNXAMqKGRYaiDoaEGQAgBHUEGgBAe0gRASDCOv8" \ + "NijCH5HbiKapzzQjhouA6MQFLiGN2g85EoRSdULj4MpRwDF0XuDnXc2ccYPjlmjuEBMwSjdvQR" \ + "SBM69k4wgteORhhoAuA7ZAMAoGH0pwAAKjI5AfAMBEZERr9LRwdR/f/" \ + "WBm+EjcfMXDOQBGAec8FkAsD/" \ + "AD4HLClF2mYmMaxmckRzw5pKSIdJDqsJz0ecGC0J59zOuW5Oko7MwuxAAgAAiAgkxSBKiREWij" \ + "MlLi6kaAERZYoCU2IQiGH1rfZGI+Y0W78s/" \ + "lhtUBVDXdPJ0QA8ZxyJAOSovfLiMcBwMFwRSeciPbgYDXgEeBzcdBDURwTpIjNAHpnhYuYguTK" \ + "g3unQCUwgGeARIgDDzMxc4XEcTy23Ta6LzCOv15VPDK/" \ + "MXBnpOulLtVsXBVEAxmithYNQOPXElRBBXLggBE6XkyI0JMwyYBWGMgJ6q9BkKNxjaz1K1TsDI" \ + "oyDzmvPYKiekGJIEE67I4w5XhwXAANwweMUkuGCQRwqtQSNAIgJWVdWnOzt0Z73R6kl6IqhpHs" \ + "KQi20iNlJYKzVeep1jEBkLAHjLRq/" \ + "UnJQIQC0AXjipN3gQHy1Bl5P4B0CMsRhT7d3VQAEcgya72QAvgY0MUVpGiE98OhpDWiijxBNIX" \ + "iQwwLyXWNlLSkJRJkI5pi5UlTXSULsyMwiIgAAACAhhBASEoKFEC4uToiQFkIImoYERQpM7Jzu" \ + "sG7HVhTTnGaL1ZaZDDK4tqNimK5HPlyfXuGYK5lrGByZSHqmoJCghDFsJCSBHqEkssICuV7MlS" \ + "HDpYuwyHgD4CnRASEjTEoCBEN8gwr0npGEhhokDMcVLq6oulqoGphjJoGEXHPku2pJQZM6zuIt" \ + "3uC7hCC6CBCqj0U4dfoOA9BhlLln1LEUBsTodNAZpm5GSuBRjpJ+R5Ek3R4mHnEpz/" \ + "woPkxaVjIZVmODUztO1TAhxmg8DmYgD7CYCYOzn0EaEhI3AgAQI7XSEZoyRajTAI3kO0Dr0Emc" \ + "DwBtiE5koAsZFh2DbidOjEjVWTcA72J0RKGoGj76McwC3UjFxPOcEMipPa7jejAXHjcs0QdzQc" \ + "ovE8YYN0zJB/" \ + "lAyR8mjOEuq0UkyCICO4Spq9o1McwpI4rXhEUzgwAAAECCGVJIZgmWaCICIi6ghOIULUoEQhCA" \ + "6RKI+" \ + "oEC7IVD7Oysin0xzEStE6bM1rjSClwzOYtrjuHTzMxr5i7CvI6LyeQY5piDQF4Mrwk51tqiBrC" \ + "gMwfXNWTmOI7HXIrwWSwhShx5b+" \ + "Ii1xxZywAw5EiYRxgwNJIFRw6Ba3LAcaAiL64jny6u1yTMEI5h4Jh5EVCXHiZidw8ByIsUYNY6" \ + "nYnwnrh0YRwaCjh1jND6IoN+e8D4SOgefGSnBRCdK+KC+" \ + "hVLp40uMkAoHI1PHK8Mw0UgALCo07kiQpd1YEHEOLMMAxOMQM0igWcEwKcPiOP5aGVv1DEGeD0" \ + "41V0twoN6iyaCSnJjqzbkmg9D5s02YTJgmh1GAABAJF1E6GIVBYcAABIAPjesNQUzQkMCPHqaG" \ + "5aWgrnQlAyPHBnsGoyI2J1YxnYulVRiRRQPzUSSQAAAAOICIkIJxRjulBjTIqJu4hRhEeKQ9BB" \ + "4sCgoIikBdwJxmhIQEVERePaOrBbUFKsFq9Via8xoMdRixUQUMdH7kA4dmdBkmMnwIExawKglH" \ + "DM5mAcLxjNur2OQcJDU8jgezKGMoTTCSSjIcISEH/" \ + "DAZSzCSRGZhkZQFzWOzHzIPI55kVeyaJyOLOjDeMB4x32EThqElRiLbu4hSagB4JPVGX106CQw" \ + "FxlezAwL1AmnI2OgJ95FTYTXUWJCHXogYjiJ9zeAQk+" \ + "MMzz6qVPvGfo6OkJ3T2dnUwABQIMAAAAAAACDQwAABAAAAIrNerQYWv9V/0//Xf9p/2H/ef9p/" \ + "2X/Yf9Q/1b/BEzE2jK0NDQQNcgJ14gIsT+BVgws0GAgojdE9hi06B0utk6OBEYZxNa/" \ + "YWB1IIIaiC3o9ycQrxHPWQR2Beo57/" \ + "Q8dRMQZxfcCgCgLFSCVcrkbAMF1KpCqhQAnjY0MQb5gPRhjae0oYk+" \ + "yA9IH9Z4vmUNKSIyKUUp2G7NVa66kjgVnUUcZ1EOBAAAIIiYWLIJMjJIQolT7uJM00SMFhFj2F" \ + "nNqfaGWu0x7MVi8SNjYScCA2AY4ksuMpMZDi6yzpho1rAWIcMx8IKBa+" \ + "IZaoHVE4SPIiKyR1iEDMnFqYXM41L4r2C4C2auQrtpqFOnM5JMmHkkHMeRIeF1vOYxyafjejxe" \ + "jxzDF+" \ + "B6cEJaoEZP9HoSwUAZLvROGEM8OINxkgiqB0LJCojXQ98rMUTnbyQgCKKskRBKoYPTAI6RpU8J" \ + "dfOn9Bj92A+xhdANvOATJADQqojR7XLp+" \ + "uW0OQjWYCAD4hiDETvevXUkAjpgjECjdaxVgTpi7BgQBghIvwmmQweKgROAYQAnxdyfDyZVwGw" \ + "GHRx6WToDAiaKIgB0n3G26RFiNDNjZQ4AABbyfsJZF7429M5FZh8N7ZfZo6e2YfQuMhtpGJ9mT" \ + "56PdjGbaAfz5LpdV+" \ + "gcuhHFY8dAAAAAUMYUGOJgSkSCAoGAQkH7NEkIxQUUoWgBamtnqDoQx8wwzLa4VuxtRcQwxM6i" \ + "mGqnKQgBNWW0tiGP43gquIHMiPBMTgaPsCI8ZgZqRyCZFwfDXMfMwIthOI4QjtGYuYgAwPBipc" \ + "k181o4BlCAhBIGF2aOubgG8uDBHDMz4WKGPHhxVXExgRmmiiZqE1MnGAB6Z0gaGuHOQEANIaCV" \ + "QV8tAuHgAOBFZmI3honY1a0m+v2+" \ + "jzxGOAidEZ46gShAZwqN14ePmBfHCnAAAsHotmG8h8swmg5oTWjInYPwm4BSGAA0Qz8WEOK0MK" \ + "kB6L0BHLkmVidRw/" \ + "CXKQaiJhRE56mXHICVMMNHIt8dVwAAAADQYAoA9IyMjJ7oqQTSj5kFKAA+N0wtxmQPSB+" \ + "szA1jTSF7QPgEji6TiF1WMldVeoorFzqdKN4sa+" \ + "xAICQkRAIAAIZUUE6xD1SQ9LIn8SmvZ8kzWFRMVAhJmWEhSJAyL4MrQAABCCGEfYoqBwVfvvLU" \ + "rU/" \ + "4c2L1xM7OVuzTDlVwzGqjVrEI8prXcU0y88plIvltLPrw60Py4gQ40ylhApdQi5FAHcwDkosPF" \ + "zB9RBhi2eV9xrjA6HRKoY9s4zLPO6ZcHEoCcDGvYTLDPI/" \ + "PbC+uHFExMNQWcxog2GKZ5jpUTZBGGcM2sX2mQCLtBWzc9M5oBKIhr2nVZDJEvSJUA+" \ + "bQsEDzom5Y7CBDnBL20EigLuFgAMG9AkZhqAXooQNcJrRGBoQkwGOCCf0JQICKmBWByTq0iKlp" \ + "+hN3YnvaOAbIg5khYARg0RrITBhYACXbggyTaw4uJi84xkxEBhVUAAAATK6zqrqO9dNIgA+" \ + "4ABn+NuzexMcPq+gTqae3YYsmmn248EvZQ0C6y6xWikQZGWFyJ9ZmmaCqXU/" \ + "0xDlHOc08ToQAgQQAACQxCyoKXMYUlIiAeJhQKkm7OrF0BwRSl9Qzyq+naTefUI6H1/" \ + "G6XhfX6qqwlu8+DWR+cCiM0qKDbrp2C04LrjmuaZEHM1dUC60NP6HujMTM9SCbkDmuHEeu+" \ + "QETMqpjoWG7hHjqCZiQU2DmIg8qhDnIMY/" \ + "rSrWjQTUvLdbUWsHpYRhQmVBVRamO7zK3TKPOlFoV1xChAZB7O8K0/" \ + "17piAkJSr3pA5yUM91WPSEOzTCRBYmgoSE7UhOkc+j0AGASjpYbGQLWggCQDLoB/" \ + "EqPIhdqU3C5wu9BFhSl5AjAcPBYWnE1mQsAAADhxRjzGPI0AGACAACEOgkoEXAjXOTQg6ja2k2" \ + "ozdSZmEgougvGG6nOAqNiqqCGKkxTGXF9GFQdEgAQWU8AACxYFD9ZTwBcHAD+" \ + "NizJxdgf4BeePL0NUzTB/EAIH3jy/" \ + "K7MDEAicmDsIKnqdq7MQiw6UcSxCUFAYAAAAAYOS8EFAeHGuBwX8vRSBSUAMxGIOSZgEXEBYUp" \ + "5UkCRGU6IomKqrW8MFWPsE+" \ + "p4BKLiSo5cWxwy6i2aCIvGlVzkQZhhqre1vk9GIfwduyysMYfp6kqSHOQRAIWFrTlamUtRVOsC" \ + "i4ASxy2Xp8bxI6UeNLl0JOylIcm8KoEFTxgd0SGyjzADxwVh5p2qbG7NoLBCFj0NvaP21M25Z0" \ + "CihupMaGN0YHS6fEgnRjQmpm83Ol/" \ + "jVmSZIsJTR4674wtdQXMxTPU4YYl55a1SW+" \ + "nKRe3FFAlAmAhGIcmscRzzms0MoAVaoI95esuyDgBoTDDNfS8gTmABgIQQkH4R8cepiMUvv8Yx" \ + "DR+" \ + "M3lgy6zpakJmQK0dy8SHhs3CtwLJn7gACzghHACADVAAk4JUSb0tKfjb0UobsAelFTvZ4NnTSR" \ + "NMDygtPnu/" \ + "qsiIRAoGNNZ25q+PSqThXbDzNxIIAAAAihw8RwCQFFUtKRCjmFa3wFAiFFWIV5V4RB/" \ + "aAb4d+jW3v1Ll3hOFnwiIRzm1rGKhPc3yaHNfBNY9j4PW4UCBz3NIMKRb/" \ + "Yo7Zgi3mhEMIHYwD9BaCSHPCGeE1DxXXzMxpB8z1mEzRc1bhNy7PUAIsIHwGl7vCGhk6Y7wXmG" \ + "vO4pK+lbRF1hPoXU6dtybCUH349R7h4KmaiyFcmTA3qJCNGVzAkTN2GcUle/" \ + "mWDIKhu76tydqwxRWyU3KxDUNIu59GGQwRZDu7naQbpjp7QCTjgcxcM6zwF4E5Oq0eoRG0o6+" \ + "3hCHJ0l8UhguOJFHBYmTcfN4aYJ10jKBL7xEZRcAIaxUQw59h3ALQQJ1wU/" \ + "pIYPpbVNRfRiBtMpLXDDNMLDGMhsCo2hgOWsIyvao0kQAAgRgAQAPLaiUAXBzCv5fAVKXAdwUB" \ + "AGRZAl43LF0nmR8b+TdyHHXDNOpY+" \ + "2MjPvg8HhPD5OZY7Oy621URi7stBMTJI6Qoh0AkAACgRGUm4oQC7aVExEUQLBAFERX47hSBQKW" \ + "OqROGjZ3VoVVU7GRyl1E27YQKri81sQoR4Yeehgy4+0R0LUBtGnOVfneARcIAs/" \ + "bIqpLUhiHKQFpM4F0ZjIQrVgjMZLmkwEQVhFaMIWozuVYbnAauQIQXmKtKmBVApYiiqBSFL/" \ + "NFWWAZvstNhHESPTJXWnpxhDmszIoF0OzhMNHRotGHmZkRViFoQvXSjkfR5jgqXPNU5yPBSWjt" \ + "ElhAARBjYLXHXDCEqZYhx/" \ + "BY4WCuWQmktTxgkp+6MDxGQRE1jaGRSdjOKJxwYlrOrE1EEAA9AZV0UwB+" \ + "SdMZDfoRAACAUfrDY7euoerjy7nMmgTkQxIIJG8Xql1h7lLtxbdXHcFpAAAAoCZPi1BjyeQAAE" \ + "DcvM4MAFxwRLKT+" \ + "VIFAB43jMmF7EMR8yeXuGFMJjL7UIifPO6qooqQMkhEMGbu6nhjkrhYRNwsgZmQIpBIAABAGYi" \ + "YBZiIIJmwkGkRES5RolwhpMUBQaBgYZnA933QqL3VcVcmdTKMmboWrGdB1er50kSGFKPaMT/" \ + "mePHi0xWF3xwXcyQXmcAjheOaGqx0pDDw0LkYoZYZMseDyfEIrwEmF5lc5LJl1wU5dNFLCAlpT" \ + "HjoHAxhmLPCkIliCmIFmc6G0QZ71bSoKBgYmIjkDJu/" \ + "IJRR54TLhyRwMXrDIhj0YW51qHNBRYJDEdbIAPRhyDAMC2pAEZkAPkJPXKBwMl5yEWyF4QqXk8" \ + "JTuxwXx8XMHWeFOfpIFp36ZVZnREf7pe8jT7pIFKErIuL5806oVQGAulqAwEwAQBQFA9hKQzAA" \ + "wGQSGHjAXBfDHGGuDzk4nRKiUmplyvR2CNPZAwAAAIZh2idUVVUB4YAA3AXkmytQOFQO/" \ + "jYsSSalkUYOFznb49swJZksjcxCeZCTuryriiyFFCECllZOnTHtXBLXxtLYhCAMAAAASAgJyYI" \ + "lS5IshGJCQiAqFIgJxZl9j/" \ + "333W7fpr05MZp24cSkeH2tSnMsFKXMW9qaikylHLmuSXU4lCgLP05DdRTO3rc7GW11BpMW1kY0" \ + "WI0IKGm0kx5qjFZjIpiivDq3YqSMulB1Ce/" \ + "f4dTnLa+" \ + "O2IKtrKH2mnnSRpi8uE6bvx0rESWgBr6HEKpYWSJNhuqSdKnDhXeh6MSWruKu6hyZM0pIGDjEL" \ + "xUMwkfCygCHDg3Vha6jhITx+UIAnnjjw+oylIFs7gYnnGElegYIR8hw5Bg+" \ + "hDkGCCPhdlFHjUvhRwBAFvB6i3CGt80JIf/" \ + "eAE8RviQdBRYacEIEQRAijEDrmjDFbopMWMVTUQzf7fUeI5iGHsD8pfkrfjOnUgAAAECvF0P7e" \ + "aABAEBk1zsCAF43bFFH88dEXg0/" \ + "6oYt6oj8MQl5Nfy4a8iSSCIRDjYnzE3veOwqsYrYTG4CCQAAwCBmEEkhJQtJtBhExUVExQQCCM" \ + "XFxIgBTkxvbxo6xfAvFov/" \ + "GfBvljmtJ0T8a1m0dl0zw+" \ + "u6AjM55vVYFWEIA3NcMMPkw0FeCR1DdyIiyY95XI8p5C3kT3nlAJ4C16gRnoRkRBgPPa1hi+" \ + "xICJTmMUPITAG1mlZ7MAwbw9VO0Pp9i66wIjSy3hGB0zDC6wx5wIvhNQwBeJAwcDF0TmNRZ6Gv" \ + "kokIwm8hmNM4Q/" \ + "VY0Vk0jozLWlAXIgwjP3aag9GR3qUG9hhtuoh82CAPhrnmEwuu6yhGhILBFYlQQp0UodRTCUAL" \ + "sQ/" \ + "CRAtBEE2YuEWKMOBAZEAPCBm3AwBqSZcC4BeYmABMAMBYkACOxyMAAABUHLMaUgAA2BqfXgMAA" \ + "NQLHjcsSUcOI1AfPA9xw5J0TP5A7B6GHndFdRIZMqEUHLN2EIzV7ZxLLC5sinMeAUkCAAAJKUg" \ + "ykSBmgCgxUTBDhIlAnA6AhpAWEOJXiCahabB1mnjOpaZaDBUMg6lYBAXMAUPUdc3AECmyI4DMZ" \ + "F6PjbxicKPLSuf1kYQAczHD63qQXLcAVbRu4BgmeZGLgLQwDMzMHD8yoJH1HbhohKH05QjAdXE" \ + "cjFBH1Bs9o8d1zQOODEAgAhERxsBFJrklKxcDTyRjvJPAGLtbWAm8xRHNaEgYi05XJJ2nLjiBv" \ + "giHThJpY6P1o4lBLEYihlBCdQRhaHeQ1IHRGO+UHK/" \ + "JNZMUZdUIdTGA6OBDwhgE6ToiBgCAUADXFJLZRegIIYQoijAitBpBAYB0mAfamTgAAMkqChSAi" \ + "hVZmAAAoCMFMlhXBQAAQKXfX1ZIgOk/hyXbVQAAfAAuAAf+NozRRI4/NiVcwNswRhM5/" \ + "NgkF3DXrFYBRMiIyNOM3Vpg2lW6E2NqlUIXnUAEIQEAgJNUJSAFRcFgZqFATOApLVScEtKAmDh" \ + "Nu4kLKHF2l8sUdJljntIH5tPxOCSuKl7MKvh0zTFIJ4YaqsLwIONWlytkqK4B4Qay8MgE6kzDc" \ + "VwXgXmRRb2erGJHdpmQFiLadToZYXxknY4YSzNHOKWwiuPDI7kyc+V4C78TrkyOx+" \ + "T6CpOBqTS8Xj+OzDFkFmE8jajuDNSEtGiNnsEbwOkYSMDFXAFvQPQK+ohQo2XDs8hfHcVMZG+" \ + "cej0BjCdmMwERRMUU1CL2qFzHHA9eV9RPZ2dTAAEArQAAAAAAAINDAAAFAAAAVx6YmR9Y/2H/" \ + "V/9m/2H/WP9Z/3VORkpMSk58cGtuam5nZ2tqCCObwnf0LmQeXJnfa+BIPtTG5UaNjfA6/" \ + "XTYrl1Li5wcMDBMyyEBAF4AAABUOFPjubFDzrqMwarPAKjxugADIIHebgW63cx9egLIFsQMGgk" \ + "uPwwAFbhABp421M7EjP9ohbCwHc6G0piw+" \ + "UcKuCR0nV41SQCASbETOXdcdVVFrlJ0c4QJJCMAAADPcA5xIzxWA8M6xnl8N9fwkEppIgEPyXI" \ + "3oSjtWYZYTHtnDH/Z7mKZTNdfNJ74Fp3eFECMtjGQhfnA9UYpJ/" \ + "1YvXJYOTJjany7DRgsABTFaIsrEaAurw8LkbAxmeNrAQQr05IYJkEtMEgE9W+" \ + "PA3LlOF7HhKtFKadthaM1K5xauaZ9l2vmygFMF1ZJqI6V4pUBIDOBmyw6DXHrSMHFUMWvchozI" \ + "Gwb0TA6EO/yEQB0PhIsIBkT/" \ + "RZH60dad4vrPXQTKg9UoU0grXHkSLlIuGTg8ehSXBchQwkIM3VasAXXhY8n0g36Ic8XYiWlQQt" \ + "y6B0tXAROgEgwWAHzCgAAAPezppotW0BBAOCrZgzA9ZIAbrfLxAToYGdTAAAwJlYqgErGhYwDF" \ + "BfYYEPg7h84CgC+" \ + "BkxRJo1LIaeFgiLTBkxFJxsW5LQQKPKqSkhAwCJkbLnspE5VuyqLa7pJKULKAQAAAC+" \ + "VM14YjpC4cagMhwhyKWMIadCdl4iKeFaQ8umy6RmFTzy9AlpMBGJer+" \ + "FT8iBH5nqQyUG2wAjvysAMw1y5VtnmhHS6Iul8+" \ + "KkzguhJqEXqgoS8siog00U4CTajcR2vOTIhZCA9jeS0gY5a4Zr5cF2Pa5iAwadjOK4EJtfNnlw" \ + "gDE7zGAGAKc2R1zFpZAYgw1Dohu8LhgvVmdqchRXCFiw6zuI9hYwa0n8F1dGaMeAawS6RgGF06" \ + "b2OOkicjlG9t4KZHADwuI5T8ZihdnE8husDr3nwSUgmkE0oTLIEc/" \ + "EYZh9t0hS9h8uEpMAf6MIv/X/" \ + "RFyLUMrp4HJEHAG8R8B0BAABxCQBw8TB7JwdsAICKjT0CAKLW6Cx4QDLX9fp0DfnwVQDeNqzFx" \ + "+YHKBeePLcNWzFJ+QPKQk42xndRAYJMQSdlrrmqXSplrsvCLKUAAAAAC7COyxK+" \ + "gDiOLYwwEihIqTcioqJiLooSnzad5t+" \ + "XI0uovdXHMqONWu1trehoTigttn5GYxgHG8NkOF7XrY0L9PqG8IxsjhZYcNZ2LQ0LBYlEXYwl4" \ + "VWCmQCEa4WbVrf5ckEGxLIEdx5WXWHh0h7AAEdZEniGHUFCRgpmTRgSYXSOC401a4Qh87ZYdDo" \ + "VkcJurIvWXwpjFF2RnKYI6iwwE5UUojSIgvCdauUWic5Jik642t86i2NiZAeZEMbo9DqjFwgc3" \ + "dI1QlOla7G0pQzDkLlLrGqyF0BmjlzJ8XSMs77qyLUbDQ95sTx2hEVzhs7PbdbWAT2rl4TFaQE" \ + "I+" \ + "D7KBRgTjiQAMLBEOZHA5R4JAELo6Jn16Rh7Ygwshn5kljY5WKeDdAGAIQBuAlQtDFxcAIxFvZ4" \ + "6HQSeNozOxc1Dk9OFJw9rw+" \ + "RdhHkYclkNPBnDq45JAGDkEHO77q6E45qlROYgAACAd7s4eePywOcolC/" \ + "CqoxMsaiwAiIlUUI8iJP+svEx+2QbbYpv9TO1J7H1a6oxTcxBWgfTROPK5IqK8+" \ + "JhfYfXcWWOuRXMMYiwBWZnV4UXB3PClYAWAbiL+" \ + "fAQ4UjpD1cJok3mLICBex2oogaEeRjDHLW4YbrlB5mZYQaLg7nWTulBJnM0huPIMR/" \ + "mEmVyRnVK4iPTIAvdTkcuh/2BCAaHr9EOEIuk2NVw7iAl/bqH56EwAkMkSIdiBdAZvUt/" \ + "ddH6ZCx6Qr1BEyARBNdw0azorHiMIbngNbOAdrLwOhamZSgiIrQ3Y5i0sPQUNtb5FifoEdtmD6" \ + "RbgBsb6O+VlBWA0CNsBtuLG4V7IDw4UhUelkBk94g+/" \ + "WKK6YV11wYAoDduA4B7z1+" \ + "6Lr2XymIDAACQYEnIPgcsKSbSAj6YKwjQHLBEn7INhBd4fleUGQAyxJHL5NyZ0pUUOyeW6CZHA" \ + "gAAgMcIhByNclkurFTAJ6Ll4uoVifIQgAgrxMqn+Ip0ZFOu+mBrGr5lMid90mIxp472/" \ + "h0nd3h8F65wXOGlXkxIaVWNMDNXKcF+" \ + "XWbyxBk1dBIPP6qBiMpUAyk6MXBNBqgqicXujEyd1JGBMa6QIBSNwMwr18DcXoMrWenWtU9zHc" \ + "Pjw1yZBA7ChMUXOmkEwh9q0TgRBH2HUggSegw4QUI3ABdsJl62hemlSmOaFWRIAgu3MMEuD3H0" \ + "e3RHJA4JAy2SxQaaTjfDgFxTUa5cGWDABtb1HhdBq9pox5U1RTkOvXr73dZtaG01Ebq6Tn3EhQ" \ + "sAgU/" \ + "oNpFRIgYDVYtOrlF1qXhqryu5JjoinVEAzovr0NEmAmmijwwTu7EjrkABJFseTe0sAIC5XjcDA" \ + "B439CWWNo2azpJW55jpHokbuhpj7YdOX1bnmOmVuavKIgmZskwZx4plgyWamrXzBBljFucSRaQ" \ + "nAgEAAMDNwYGJSDxRWeEpLvBShHKoaGgrMYREkqgIXGLihBBHjIWevu8pJAQEAChnMfPpE8x1M" \ + "Giva5JHjszM9YJrCWbgel1jcW906QAgsipqWic7DN82mBrhoCM6AIbAkPBf6IgFn5zeMeNJWDx" \ + "8KEGoy5HBSoTj4WyAmmIYgqoxNSwRjA4GufTGBUd6KwcFnATQ1y4XgEOAEmix6dkdRq9bahQaI" \ + "N1+iK2FyY1BI+" \ + "sIjbZGR1zUQnK6vLHIIEHTNddrhms0TqZnCEkNSBjXzKwCciCFRabeI9qEQRQoqAc1FkAcp3A6" \ + "S1QoAGDryQi0GOjDGwCWhRcUABAGLhv4IIR+" \ + "v98HAMhIAPDppHj2hkWVUlFR2Quwt4tuJIMDuMAHFjYkhSzy/L8ZsuVnkSupZMresCEhRDEM/" \ + "83E+" \ + "rPKlVQyUfdHWVGkQIgyiCJFWYQKZWQxNdVNvQGScooeYtPZmEpKYhIAAHCFggYx8hmGFTMyw6X" \ + "kLgFRCEILqUX4oJUQBrvAvUjJfibTT6cOHNjFTHG87DxduU7Vh1wXTK7hOK7VXhOurRyPXMOkX" \ + "HD8VhbhdxsEhiGS8OKgRfGlrbZqqHavXmdA4FQYQgIwL1AO1J+" \ + "p9k6nmV6iJngxIQxLwwDMABxzw2++ASgxOXL8MvPI9zEAkMkcx+" \ + "MgE8hkAikBLf7alVlCnYIwbGfGzNqCVVuZbUbbzCE969R0OpDQkBYIQQ+" \ + "GOGKZBm3P5IkRZYC5kDiu65dJEtJipop3Fi9mhhkSgwpHDsAKOQpwg3FnuhicFPXBW8vpSlEFW" \ + "AEAd6w8ecSZhJ6bwBva91t3Avs1S7b8yQ8yYqiiowpTDXokAFBEALgsFJz7CeCaU3JRPVysbc4" \ + "KAPABPAV3W0MRxvdyBAX/NBUV3OUMRRi/" \ + "nyNEch+cSQ2VpTKTNyM0IErFMTvMa4a5MpPhQS5XZL1D6KOyvLBW74Q+/" \ + "LZQzerw1OCbEgBoYNcAlAULDSuNVA8UfG7aMmChYqWR+" \ + "gUFvxvTmmpGGXhTFQAawExCsAe1iDDHBw5GF1LvNJsxFm1CGgYLtMU4GJj5ziFCfRjzAmQBdxi" \ + "QcF6kggXbPLaC5Q2wuD6kggXbNPk8kQApvRGEYqj3IW7UlXAthj2gJnE5JPiQ60gy4RHM1ZBeE" \ + "I7wIsIAnjgESCiQYNgZpAW9nBhHsr5Ach+" \ + "YWgt6ODGOZP6A5H5QfwF4cwRTlAHf6yIWknkMlIDhADBRk/" \ + "k0pFCnwxsjESZ3GmFcMnA9Ul8YE+" \ + "JlBAHyP6UWANQBCyM4JH8kgZHkfsg6YGEEt8SHzERyL6ijogqQ5j1TUFErK+" \ + "SzxPUKcwtIRPgdgWuG11w+" \ + "OtTpLVDDZF4zx6knBlUAhwDICmQALCKB08VwOgaSu6apRiR0jpu3QyC5zziNs7JaCAqQZFQwMN" \ + "c5uY3QLbMSrgmNzxINUxl7OyYslrwoYTSW5hsgvtFBQqEPnf1YBIBj6z8BXDpDN7Ff+" \ + "Wx9MUF2jU5VgT/" \ + "1w1Es0H+1J1qEFCsX5TGX0IZvTtN9S5llTiNyVKec5rJmvh6CYjGTi9Bn4tLhaREGlmcL5nBu+" \ + "KDZnq2Kiol8RH9qfpfQf6ZSVK0dP/e/" \ + "nvvUaGuZNm3KVN++ZbvTOpOdjzawOi+O9yoXAHQ+0RXo8vzCWFyz9/lEVuAzf2EuPvO/" \ + "sjx0WR0hcqc5I4MaznYaOVWEsijZZpWBique7CZhMbGQZfEyWVyG0LMCYp4Oza4KUjZViMiSQg" \ + "/JWxerNC3JT/767BP/xVSfXYYZZ5wwxSqfPgto4LprbSdcLtEVXI/" \ + "fo3THu1TCFeTTKaXN2VbNrK4qg85GO2bFWkWILNyZbK/" \ + "KP1NAdz0CADFN8i4ltJAISgLi0X9a2ynottztoFPoZvWfneL3HKV90HsypJK7Z4GTo+" \ + "rlc5fjyjLaOUypO8x83scuACw2qQ7qtd7EAgV+" \ + "5BJXoPs91L7p3TgWRSQRmxcH564OZydzCfU0SV57KRj4ToRDUoFkPXZKTjVV3okKiaruMtXOiJ" \ + "vA0/zjx//HqlnfY8n74+l7HveURKJzFxWIUOel4rq+e5dTPPUjM/" \ + "23WFUCjErBCfplfdjiwjcqhRcQp/" \ + "qYYjPcVVZEYmJ2CVlVIYnssMHUPUuHciRXDLCaCJMScvjseLhRLq8nNuOopQoxmYiKv4i4lWhP" \ + "UcrfRaso7b1OeIiPC09dSCl4giLiB5UKKWZX6D2OAPgkAGQ+" \ + "qQOs25VQbOBdQtEVaCs90Di9V11URiLaIeY5Y8x+" \ + "s01mCsGpqjlRM0w5kbtfAbg2X7Y5ooTqyNL1iX234M3noeyJB8rDzTbe/" \ + "NOZnDUSdBctE3OXWHOV0Hru/" \ + "z8zXbkYTn+tVnNvilOx1wIAjE7FAfSov5nimu1oVZtB7Pk3U7xm/" \ + "4zKyoR2Zhumjfs8c1oUMI8ngKhITnHK41EyJeLu3qyMogICupCAL8aiNMUQp0VFiFBUPJj4EzN" \ + "25n8O7Wc6Go4cOdiN+LLFwUwc+" \ + "AAVvvxKAJxS4QD9dn1JxTFeKhXPoF7uDxSL2dtlecgRKUQVisiLrMpA6ggbRs0jjnKEfgOgpdr" \ + "GJEmCSIha0HPVlKMFk4QiE4JF1+bDDBGaRT0YxK13A0tkcaGKi4iAYFT+/" \ + "W2YfgarKWpoBQCMUsmKtB5iEFZcwyutkg2cSxwhWlzjv1ElEmfMLMrRNnpJm5k5p0iVFcoR4QY" \ + "AaqZoCY7B2Ht3D9GSj5UqYu5SuApSXjaqRFGWFWVX7g1JNJIsdMVGBnXsrEM17v79bbU6mtJ+" \ + "1MbHl20AAFRavZMB3cRI3ALd8C+" \ + "tnmHEN3GkbvHDtpvihMRpzsEZGciKmJYo5h63sigOZkVZBsqSekW2KwFcl6qEF+" \ + "YzIkKKQctyeZlBtNogIMHzUTQ571WEF0+a8GJSCLgRqQb+" \ + "Ga5LgvTOmLssAABPZ2dTAADA1wAAAAAAAINDAAAGAAAAssrB0Bhy/5H/Yv9Z/0j/Wf92/0r/" \ + "Vv9N/17/" \ + "VP9sOrUDVEK81OJAtxnVTcDvIWrbbP+qRVGJOCSJg2sRZVYUgzhr8q+" \ + "OySJtSwRgYVvWvMClNgVxqmmBq7XQR4pyuSiRllAhTU/" \ + "J80pq59H9GYf2dEFIWTNjavbxba7fe+OzU2w2vu0F347gu3zb2vq6AQBaJx0+LCWy/" \ + "w6fDJKL0EZwnXRoeIwP/" \ + "z18GjtrPSVoJb4DAJBZWQaiMiPZj7TIXiTY8nimN0md5gjdTTmENAAJAAAAoAIUURwBLHQ+" \ + "h8sTCIWiPLNMeQmDxdwkmdC+CLzUAdMyBAIPBUlZIkwN7fnb+" \ + "9uSwtRWkmuyyocmOYtBEel1HmpeTJ1EVLFOb0lEnO0AXMdjMseLD1Mp8wDCL2uEL0vATRNAZyp" \ + "a49olwNrQqAJwT3iAkFusfXgdw8VZCVX8WMGwTjUUFEPpfgfmOncd+" \ + "rHhdRSxWu3EBExD0NS2mRys5YNvKavYqwyfMhfAhNCeup1HCj7GrEZVA7mODVjh+BT+" \ + "9PvuptHu3jSigQFX1zBEmbaGaDb2JQaCVRhGwwoyevNdOw9O15QrnybH5LqurGVP20xayn4QQB" \ + "FRLNMQF1QHHcxIBM6wdmFYKUG3AQBwGjQAAAOnZf1Ub/" \ + "e9v37rOD2YuEfcx5PCbDSLZCYAAKCX25caq19ul9qapX61+" \ + "lIDABAZSVxfXy8QCQBYc3MAHjkFVsyd/TfmSw7VR2o/O0v5UYpsnz37D/Mhh+wjsZ/" \ + "V4v2goqyEoiJRoSzJogx4HDqDIpEqE6EqJWUi14uAo+4E4GnIM6qbRCCmFwAAAABEA+" \ + "qwXTLOUhBj28gygOcTCBhaGosRLslN8EpBjMajIMEsUkAQkyAGBEIBE1AQJ2AKYAGLEBqgKIqi" \ + "mTCID0NNSEaLLkYDnQvEN18VGb45rkw4rqOnYaV6GJ2YACioYLGzmqZFrQ7FFAciIOiIIzQAAF" \ + "4XqrYWe4vVBsVQUUGGOSbXcX14JZmdaYqKKIiCoa1NBDF0zSOW7AEDTIwyGBNWY8ImffuJWy/" \ + "n9DB6AMBHPXz4HIbqDEUEtccwTauqOKLOMMbD5ZrjmhnC9ciHmWlnscFiFSyY2IsDPPLpyHUw4" \ + "TpYALQIILSubgSg3+/3+/1+v98Pow83Ajh3lCBkrhyPYzJCnQIAAMgovA6uh9EJAABeKZVWTb/" \ + "60V7qPRw9u5P3C16lVFo1/" \ + "epHe+" \ + "nu4ejZnbxfsP4ga4RCkllZkYEAOTnnefRqj3JmH9iZ6QAhtWtTGgAAAAAAwhLwGSFHCB6XRMWU" \ + "R1nKslxCGD5DweeAwIEwXKCZoigIBdNuXW+" \ + "u6XmVQBqiHoQyTBsxHNkLpmlrZ6uG48Prxeu6Zo7jw4cPQ/" \ + "hDnTowOvIY5pjhOq7rypEF4vTGQlhZ3fSY2+" \ + "AgDOHWUYPHMMxFEsRQFDUNMVA7Hz7l4nhdwPAgwKFaUbW3qGCxcb1CZsJM8kpAaJjj9bgCwwBR" \ + "mPCFMYS4rlcmBwPMkWFyXJnMdXDMCz5NJvMhMGGA2ohpOCbqmDghiE8PAAAAAAAAYq9qMbFxYI" \ + "ppZ2M6hmNitXfwIddxvSbD9WEAo402Vgdq65jjamPrUAXEsLERAPV6TcJIAOBooXUEAADAsb5P" \ + "ql+mBQAAAN4J9ZZPu/wP45f1bp5ZsmuFdUK96tMu/" \ + "0P7Zb2bZ5bsWmEPRHV1EDUkqbKiSFFUhCiCuV06Xs1G9VyLiZQHAAAAACCClHTIhfTAEMK3JKn" \ + "VdmIjsNo6ZtoYYmOjIlgdM0yrKXaYNqjVDodWPB6QA8K8rpnJQcJ1zeuagzkWqKckjAW9g+" \ + "s14fUaOI4HUEYXITpqIkAM1Tu9jut4TciVMITJ8HodCWQ4YfQADOANoUR/" \ + "A6NjXnPk9SKBgymYKphqWGzAYudxQK6LGQAA3kSYCJcdDm0xTNMiiAiqqoIhYie2IoYQfkcA4C" \ + "lghIT+" \ + "Bpe3CDXtBNSRaasI9qbjmjm4Ei6OeRFM03DQo4cAiLFIunWAd11M5hWuOQ4GwhUgD8jBNXNdV3" \ + "jNwQDAmEBHABgTEWBCWgCgowMAAACu49OHa7GxcYQDHAQoAP74JDM9PfvDL92dfFUybqQ9Osmw" \ + "p2V/" \ + "+KW7+" \ + "apk3Ih7oIaKDBUQUmRZGRlRU0QRe1pQ3VN5Q1SKidQKkXuiGAAAAAAIAsFdEIKLZMmAkwlyuWH" \ + "BXaYIxWJioCAUsIhLfKqKWkVMwKq2Zntcx8wxuRVpwpV3EW/" \ + "RRyZeM2FWCsOEa3g9ZmbIMLQY5qwMc1zr0sFAGCMMTGskbXXMFAO1mmJgimGu5HYFwJLEawIzE" \ + "NapAwBQFsf1Vjzy3QFzHZk7BTENW7W0VRxa1Cr2zGCrPVpMAQDAIGH1DomxGKG2aoM64TDT12C" \ + "L4rolxwGE0UN39IchwOvxOraYx+" \ + "t4bwBdhE5ADTUxHNjZ22LYuD7NZF6v1e31zcx6zrEAKfTQU5eHAcLoXQwAANBiX0heAwAAoxl9" \ + "XQLY2gCw6koAAAAAwJi5JflldKc4cDAPu7NKAQAAANB9/7YUAAAA3vjE1hG/" \ + "eISH7tv5rFmwJWx8UnWPXj186Hb2SbcVu6ZUSwQ11CIzK5AZqMgQaTcwymUaDxMWgKQ8nxgowo" \ + "TvGRllZlE2ByJDep1kiMCIE+" \ + "UBAAAAgIDwJjjo5aaInToqytXNJkNBGrxRElEWdZgihGYwiJBLAiKeyfS0hGkuioJQhMoSuMzr" \ + "ifT3D1OQMmGwWLh5ywBRCMhQ8o93apqvqaryMorpq5x0LBQTAAEph2La2hoGlmn2FityDMLrGO" \ + "1k2HS4PjSsrjAd6pDRJdE1kmsZxyOv445hcjDHcb1FHbp5aFF/" \ + "dXoE86GG6AwGkxNWEHOQAIxqxY/AcAxnFJmGJ1YBFRcA7xhC/" \ + "zvtjPnm7M3up1AAMMTMAHIwn5W15st2SgAAABBHAABDVEAEI6aaXWD0AYQcpaGbD1/" \ + "DiJwhDDNMAgAoSgC6Edx44wDgdcETADRHRn+/9/ern6+ur+1+2d2c9XF1M/y93QAAAABz5/" \ + "ufBgAAAB7pNMoRs33saBnTJ2D0uEU6jXLEbB87Wsb0CRg9btdEVZFU1ZRRkYQkKjJkcBKGoBBZ" \ + "GYkIVLmatjWrg/II0wm7yQghJiWSBAAAABGzZJAgIkjJLOEKWVDK4/" \ + "AJhyEMiIBQzKJMWCBOUQxJLCQJAksWzJQIREDTIjSIOAhhATgIPCYzx2WROHU0DBGOBI6EeXDM" \ + "os5pMQyj8SAW9RHUMOLINUNgMpPXI59myDDAFfDEIqN3MZhqbzHEECVwRwQAAOSaDzyOJHMk4f" \ + "jAY2axsTPV1lAbAYAf+" \ + "k3sDkTFztRSiwmmIWAegetg6XowdKMwEQEAiqiomFaHtjaMkWgoBYC1NYDMdQxvCiZWB7Zib2J" \ + "abEdz5HVdHPPiAbwMFgyIk+giEwDAWPSOdMDoBwCgG1aLAKD3/" \ + "S0AsGO+VNLeqaW8tpS3+UVmUQAu3sjU/" \ + "B47Pgb1zr4gS7ISNzI1v8eOj0F9Zl+" \ + "QA1npfEjVZYYaa0rVRNRQGWrImlA4ZjEcgHY9LcA4EAkwZjAbgKO7qkiEGCWRAAAAAAvBEJJYN" \ + "CIpWBKxZLBkKSUTxFmEAk0LAEdKEzEhBSZCmmJCiwsYDEIAZjBFgWkBTZiIQxxCBpimCBgOM2j" \ + "QBG6SLpaiysW0izAYAAC5wkCGB1y5BnIM5FNmCF0k6Bn0kYBPB5kjgWuYORKO12hXCAfAvBhIg" \ + "CnUYicihokCqgKTSRIyk9cUTId2YkFSAwAAwGKk0vGFVPi8tTC54HhNcj1eTwVkwIi0LkXKBHX" \ + "gARgOrgw8hmMekwECQ2YE1OkkobwNQBYRJnlri2G1SA2AEuCMFBHhtOCQGFAAiKOBiTjV7/" \ + "e7fToicLYBgA8/I0JEbe2tpioigHcsJAMsiZ8AgAuwcgG2EoACIAP+6CzKkpRfTI97tbNDya/" \ + "RmZQ5Kb+YHvdqZ4ew/" \ + "zWFWguhfSWqoCpTlAWE3diQ2hvp0wIkujNXNRLCeKIYAAAAAMCBv2Zuo0sBJbg8DkTw+" \ + "MIwCAPKZUDoEgUWEfeCkoAQFJgmTEGUov0iCe1pGvYq9oaihtqiYo4DrjDhFcIV4IKZV2CuMEw" \ + "yTBheDFNs7VUEBdRUwFTAHMGBaSthCCwAXucxF9fxIddjvjsOVqc6EPc6mGLYMgmoAAiYgfC6y" \ + "DHXAcOiFsfUsJiqVjF9yBwEAACApje6qBEmAvI7gAkvXjmOxzsdeQKAAQBUUNbBysytJu3F1oh" \ + "FdRXJMVcAAGC7PGFxhSQkBPJYgAKeXkcIMqqNlemmODCwEdsIO7BcFggFgw4oIuzQE4QadIjBy" \ + "mC0xnhEg5BQBVsDh6JWC6oC5pYeM69hmNeBEQAAAB4JrdaYSBfbx+ys5DS/" \ + "SGix57TsYPvYPTnN75qozqCqOlNZERTVJUqIPCezIGsMImlqDbVXjk54G8BQMRM3y4BiAAAAAJ" \ + "BgiuzGvaSw2COcUPAsKgwQtVLFCAXMjE5kisGCZIk4oKgqJGEATADaK+" \ + "JGBBRFi1B0EFqu6zGZD5yK7w5uMW46gNdkroOZTDjINWPlOCJ8jJ7ICHT17Ywa4pepioEgZozV" \ + "BLVrVvUp6wihFEbP0BZBDaymyjQRIY4Wp6b5bo4ClmwhM8OlruXD5FvIACRHyKeT3e6EEYKKho" \ + "TgnO3b67hOKQIAAPir3b0QhhICAL2E4x1zzGxcR0ZmBTL9qCxGrjFhjGgCiH2YOhgDKmJ1YtJy" \ + "8xUAANWeUkgZSecbRxsRUxXt4/" \ + "QEAOR4cVzHpw8fzDoAAADb26LUAxeXWn2M4NKBCQDoUMcAINMAGPK2yQOAifAxxggAAAA+" \ + "KZ3VGs8Opl93kpyTI2xSuqg1nl9MnzetLicOuR+KTFRDGWR1VZCgOI+" \ + "IyqoIMjIydNfoqBUj7I2ORdytWZAAAAAAwGcIFVIOy7IChku5BIQGoDEORyB5hHg+" \ + "SUkMFpICMXcoMEXTghIlQiAUo4itMX/FEAJfOFaddCzcisxAVuDI5C/" \ + "BAaNwbWSGqmY1Wuqa6qt1FdYsDuaCmYGZFZiZH9fr0GEiFBWq4Sm2NmJVe9c1lSEBFLg52NV2T" \ + "DBO43HCj2fCdUyAE2rRWyCAs4Au9QQnccwQqfhskEkGAAC8ziIQui0wGgJE2NKnM8J13MkY5nx" \ + "qwUfou6OV0d0LMDwVwLTgyyL+Ej+GKz80AAAAoBgiiJNi6+" \ + "xj8W2q1alYxUCsgqc9uAg10OkoJT6yU6oYCBAAYHRFEAAAdAHQIgEUYbVPMI/" \ + "NmPmU6jRQDKvLdYsxZgD+" \ + "2FxCl2VHjIAbORGNzjnjs7IRze9PTPwQEyALiQoUlQGC2M8D6B4a2gDjkw8gOmOPvoGFmN5NSq" \ + "wAZwIAAAAARqNG0ysYsTgPzUCEQgGPbxDyNMKwoAJQCIViREDcKiihCO3uEyIAGJRKChwPdciT" \ + "pAyT6ODP5QiJiyJgGrSQVUsByq8mQcmUBD1LHlFhQrpERCgFAACgKRYpwNNkVKyGgGFYZTINPA" \ + "7y4rThFo4ZOKTMgxkVJwwTmEy8gKIrjtdR1fV7UYEUARIDI0fx7TONAC3kxOjfGM6YmExpYh+" \ + "EtSpleWO+1p6ziSjXSooCmAZA/" \ + "okWJjoAAAAAjsdJSrFPZ2dTAAHABwEAAAAAAINDAAAHAAAASEashxh0/2H/Tf9Q/1z/a/9J/" \ + "0P/Uf9f/1v/T/" \ + "9ThxSNkAfKZZHcwvT9bk446gQtnFQwGkw88zQxMEa6wgUAkqSiKb0lYqgtKiLTxMTADHOsbcED" \ + "Zm1ioq8n617fHM16LwAAGP0g1wOAGQAAAIBgyvQuAVIIB8Cnu8LjCWrwOJgd1o8nEhcDDFVkEm" \ + "Kq86YqAF7ZPEBJ42J7xIdyju+ubB4gpYuL6ZUfSrWo+" \ + "4esKqACKgURiahItg1YTZ3oE9W51Bx6RJw2sAAAAAAAh4LH03mEcIUcygFEWZbH53A5ABEh0qS" \ + "IuCPOvgrZEaGFspgYAAAA8apAKteLn8K1JdekzFHpUjEH8HoG8EqmugZMOSBPYoTVU4suGL2ip" \ + "YZOnSLCKJigBghq4LU5Mc2XyVgEBoYJxFL1LmksY7X1I6CYFsMYVG0NW8Pega4AACMRJkZG6KA" \ + "zYbN1aDvZDk9YDRMQJDKBcxkAAABeBJiTtSISAuEL3V2/caEUAD3l/" \ + "cT1UdBtEd47CaATYAAA5l8C3vtLABCAOegkFk9j6hlTdRQagUCMdE2NlwJDRyUrsY6VUHkxOWW" \ + "UhXnGcZ1uNPgARFboAyz0Xm8AEUyIY6K5L2MLIawsNgACDIT5kTIdGGJYLU5FRI2c7lWkCSTXh" \ + "ZfVygAAAAAe6dxyPkcfmstFTnVHOrecz9GL5mElJ3s/" \ + "kEUNkIGKskpQBEUlpuoa6ctkB+" \ + "iJpjI24ngAYQEAAAAAIGDBYSgFjxIenwFLkJKEFFKyEmDhiEpApsXZAcUEDAbNgJQI3URYnAaH" \ + "VAaDmQEAAKyObK1iUWeLTF92Dm38+" \ + "BTCiyuP42LIMQ8CxzFzXMwTRh8B43Q5NIQ8eAWFwMzj4G2Ti8ZFcgAEJhetHNcZCRYI9QDUtFi" \ + "nU7EiqhgCwKm3aABex8yDYzL7oYU4RkCNMhGxYrEaVos9FQGYAQCA4WR0qAfgdDoB6FmMw4gIZ" \ + "yQLFdCFYdQDGP1+H0YYQQwdpqmK2IGlTAUwFzfkYODIhw/HNddjVWCxlDDXfeBI/" \ + "VySEdFdiNTCAJSg6qC44iIrRUYLeidggE6tgQgAEAgXKCqLYm/" \ + "YCDbiC1UrkEshOoDBAB7ZvIlUHHNQXOVBqCSyeRUlS140a3sRGvlAZk0yRAnVWVGKjAxRGUzdS" \ + "Xmhh92Qk3iciCQAAADAY20asg7iCcEXgFCGAOpMEJBSMZtMMcUik45mOjVLZpwwcSFLOR6PB5D" \ + "hjluXmVVDXg9mDnjADBNpgFl4XFytJEGCT8fkejw+" \ + "JaNkARjcrYwzDoRHgk0Yh0RHKWW0ajhjznLYjSUxDK/" \ + "Md8cFM0MXKMZBXYdrsGEGRFJXrfBtHac4g2oqhNWgTPAUAABOoSKElcjjIqkQADrgzYrqMop5Y" \ + "Wcni7RSJtA6qrWegui9kxuENayEOC42dzyLXDABFtPO1NJABa37q0EHjB+yz5cgQ/" \ + "QIT4ALk0ObCELMcSJEXRGM6/ZtpQi9AOPujcpbFJfpDRH/" \ + "TgwthqkIhpMtGMZQUIlGULX6rZiuB/" \ + "B6DADAIwBpEgCSVQgA3mi8+" \ + "5gyE3Tt01gRGbNovLqUliao7dMwvEGNVYlItLlhmdt1UT2uMFGReNyARQjKAQAAAMQQ8GnrgM5" \ + "huDEOGK4nLWBIuMItWURSVFZCEUI4DQBUXybOjpw6tBF7U896yhwn2j2K0c/" \ + "EgAVcKxNWMI8Z3LqejHpVW1Eb5kjIA1NpqN7orXSMevC6HpNVSNFhAkmOMDnmw2sq/" \ + "UVhZipwcYDSb2AIQtRV7rgNoFy3SCwwA6ZDahcFYXG347lwE/" \ + "NVTKjELdYuy5eJm3qY4l8FBQH4JFyAGYWuKFcxn3INwMw1jBOGATpa1nunK8q03an3RC90jib0" \ + "w40xMEZYUetcOt3odtA6OtIxwIXEzPyZogjSHCN2uJztxPSq73YIGD0cIjJhCG2ioxVogIUJjd" \ + "a0ACgkxuyQamJrb7EROxsdVbUs9UwGyVu81z48Kq00ACCyiwAGQRgALGu8AwBeB+" \ + "w0ZdKMjF9KFMjobjjSlMWkVHwqYRG+" \ + "oagRRJK5xqaZ3l3eVElV6CyZESIkAAAAaDTKHTwifEIjQpuFoBBjMRFSEhWyoNz3CkWsM6Tt1A" \ + "krFnvfzn7sygxqu08G9nYWBQtuLSEwxwXXaya6/RhaA/" \ + "NiEvJ6VMqR6zoyrDbXU8UM2jHXY2vSp8w8HkcZWbTHTCYH/CZbYyBXjqpmwJbGhCGsXDPXZ/" \ + "geTrtyUIbRJq2maeN/" \ + "YnoDtYjFWuJARAXTAiscRzIzM6mUokaLBpWDa1QSA2zZdBksXOgD8CBBjihLH3ywlp4YFqqHd1" \ + "owTlDjlIMArNJpSxgLKzw1Xo+" \ + "B40U2ZqOcUSIKymQZqsdydKjwiaw40pmcRVqGTWEFlcIcGdYbQy/" \ + "I3e4JY7MvDI1R2IuCTRVAt2uUAWLLjEhdpgurtZEItvq6sXWj03snCRNW2g/" \ + "NADYxQIRnpQAp2w8sFGEJ2WP0SjsJAN43nH1J4wAW8NA3nH1JcQCLAjwcmQuCgKkyttcuKZeoi" \ + "SASUiMIAAAAZDERUQoolxNikjQtzhCnQIEIBJRoOaGIf3vTfgYnJqzTWWY6zUSsjYOvKuD1sXi" \ + "Mqslr0rhIOK7jRlgkVGcYsTos+LAwC+" \ + "qMAByG9kcbIojamYKhIipOY3yEt3gAxzETJvmUueY1AwDXNeSYydGCa5hDy+" \ + "uqvSlKG6jYmIYVcwSO48U8hmSYyZGRHhdk4y2ZYCJRIsfoaYQHAyMeiAQ9nKsI8oY4kxOIEILQ" \ + "lVBQY0BAAoD2m4uIbsWRIxc8gWu0myBaGsUowfXXk9BX+" \ + "4kLKYIBTgM4hYGO6p2WDgEkxlCnjkSve+O4tAkAABL/" \ + "7dSQZsbc+VZbCbmdQasaeBENAOj3GRMZALlKJByPAADUxM5qihoKQGInJgBgbHkCKAB+" \ + "N2wpBh7I6TIXTuZu2FIM4oEcLnOBZI7MYJGnNDFVjZWuVFwkZopbDBIAAEBETFzoQVOgaBYRCI" \ + "WUqDglIhCIiFIUEaXERYmIOrCooPZWbB1Xi+t6cOV6HC8CIS9mQEyhbhzjPWOooTC5Ji+" \ + "YmUxYh4aCBgzRGwpWmaoAouDPdxlEGEON+e4gmXBdjFoEQgIkTl1yhVpFckb2CEl0Ou/" \ + "IHOERyAwh1wWZDwckTKQEIKz0pgQAAhMbGQowUgZiBkm+dz0ACaxoLwLBIfVO/" \ + "fUyIlhWARrJjayiTyIkXIDOM8K4/" \ + "GFyE1ZgVqblQ11wSfg4M55AR51jYoTpcXIMUb91xI7QoQdMJPrHBGijj3C08L3YETVl0OeA/" \ + "F0BAGAYHAEAsxkwjwtmZgmAFo/" \ + "rMdasMTEBZ0xMoOi9gMjhpwBIhfUCnjccaw1yguphjce84VhrkBNUD2s87WbS2rmqq9vJ2BWHZ" \ + "cYNAgAAAECcmaJEaZpFxIlQTCwOsbU4YWOxmo7ZGAaO4RA7MR1XHKhptcWwE1OcUDHFVDFU8Ch" \ + "cXMSYTISxqEdEZFuFhLxORS4UKnBhdFFQPYWbkvl0XJnM5DrgMTArTLgCB7wyAJMJCwUiNtGFA" \ + "RwcBObT8ZjAXKBdOebT43Vdr8eHTy9gyAUXGeb1AgLXxKHTh791tTphShMRY6VnND8VJ6I2gWm" \ + "E5vXS2IRGRA7G6Pplchz9iRiatbSjQw/" \ + "o9RG0BOPhJt+" \ + "EXHNrC6QCXPMCIEOYERci0McYOkaI8f8OROg4CHSghUEXiAC1VNB7E0rHWXV0GBecjhRMgPYsP" \ + "Z+" \ + "tXz8AAORpKHpnRgT4PxUCgdaFLn1bfpig8MvGGe4AAICpXi8pT3291qQDAAAJPjeckw9mQbUYE" \ + "jKGueGYYpATxLywPV+" \ + "qRESUEBkZ01Rd7ZVLrBNmEzsAAABAMpGQzAxIFpItToiNvYkVi42TDsWBGuKEwDgMtoZjlolJG" \ + "9/leqTFu/" \ + "hYLbhrsDUFi+" \ + "2gE4PqNFSm66oco14twjpVYgIJx4MrJ4Jc64CLFRzHXMnA6whruS7CDNerpnTwllKYBMhkOkAf" \ + "HdGKQDgOXoRg3UuvCqomZ82ka2hluODUte/" \ + "mQC3fQSZMRKaKIioGQ2O1qNbYwNDtG3YREyAASUQxmggzACoi6IUk74JHXC4n0XlE1ofNBcawX" \ + "B/jNOd76rjxMMSt30R2vaduv/" \ + "TDcquhxZXaMUwyR3gRYoUAR0fpACapOgZjAMCCkyAl47SOQlQX9UbPrclh/" \ + "sCJYACWn7Vu7Pf71cnkAQDERfpnLAC3RVrsh0GH98kRxlNG/" \ + "F1AN2RXSareh0aDAAwvaVKAkwA+B2zFB9EghsWC5zlgKz7QQFjA87EHkLpzR+" \ + "7Estuuq7vCLnSOscwcAAAAQNFCSYHAJaDFaUlKICYQgxBi4mJiAC1K0SIQZZoWJQIxlEOMiNC1" \ + "3a7A8I+NjcWwtZRD0/HpuI5MjvCaYxg4dBhVTIVN77SlMy69/" \ + "gbXoF5YyVxpkRz5hYHAXCThINdxMDwSLua4huR1JJCoMxiknUEnYOUAhrXVOLi45jWTj7HFNWG" \ + "OPGocAzOxMrN0DWQCQxi4eB3HxQHizLNywJKAOo0nkdKAcek9a+" \ + "ttYcQ2wQI3wwAAxngnwSoMHDMzxytkDIWprhvO99dwNHc6vccsXRczx8wcr3VjHLH1O0rtnxB6" \ + "3sscGhAXHdCDtHtPCXQMhFAn0QGI7FSPk6M2dPvdgVupidB5eFdkq9M3Af3YoJt6QJKOEHCyp4" \ + "ABq0sII8C1Q+" \ + "oi6RwPj0SuwCfI9SIAHjcsqQazIHoY4SluWFILdgJe4OmYEAgOMxwGCEBXJ00nhuOkkokbJAAA" \ + "gLioCE3R4hRhSgChi4iIikiKijBFCcULJhCKCsWZiHCFuyjEiZAZbpQog1BgIiqkaQrPKryGa8" \ + "gVjrwIk4thgCRzzU1UbWxsmocJkPBccekpoJQfUUgIrCJK1yk9gMl1RXL6MHoD4hkVjHNFYYWB" \ + "ImpdNILVUdANYbGI+S4P2CBj5PV4MNdk5mBkIDBoTLOmTfR/" \ + "pze6Rpu4rWPwyMAMMFGnii7jVkOCYBUlMpGNNxPDCEx3UUfOMMzxmg6xYM4viYQWQykoCaVMMW" \ + "uMD+l9GL8jtYx0wEVIS4uhGUJP1gZCCIOOoEVYDRgyr5mZURKJDkIGPkPr/" \ + "shgABdMKPorPG8xRIFWCmF7ROs1NMAwwbswp35SMgl5wpgBw3y5frcAAO5RAZ42TC7GyEOjj5e" \ + "lNLKHtGF0MYgJurhaF03q8a7OShARqchMZ86cOU81OVd0V6mYjB6K5RGQAAAAgCQ0MEsI6sIFK" \ + "XDJkiIiQtGKifLpycL/" \ + "qdOjOVkOjLR3Nm3sdq2qatnFV4pTUdT00l86gWIRFNTE9QjDjIIwSbByTQ4Yrhz13GmBQxfhFA" \ + "RxAkVQVBW5rr8Q5mJWt0LlxBSWFpSZXMegNPNiDoChel1lNACvYZjjmgDHXA+" \ + "g0ooMHEamoYDxMsdMFsKnUEPH3DbR6PXRFLpI5gAqkWRQwcBwyjh4FJjjQ0NGWCyRO43sAacOQ" \ + "7ikYB1UbzLJKxNGI1zXdjAwsE9nZ1MAAYA1AQAAAAAAg0MAAAgAAABdaUsxHF7/aP9I/1T/" \ + "Zf9o/2VCaGv/Yf9P/2xFSERse/" \ + "9GhzM8e5fRoM+b5OAGfAv2HQqqOog9JsfCj3TjREdf7JhGeAIAhCGAs/" \ + "3wAwAMjwBAC7pdHQBPIwHifACATdHdiX4sBayk0G+" \ + "19VdyEWOOLXK8AdCFWC0f5wAAFOAfPgesvcRQqFgV4CluWGoKTKAt4Olup2ZJIMvISBlWHnO50" \ + "fWUcDoWmck5igUAAHBjumQpndi5qSAu+" \ + "2RKnKK5XMzEtEIg06kf66Tfx4ZMz1Q3nfoNprOHC5h+kTtLFaUxhFFYm89qYmLZhKF+" \ + "h0YQywuvFabB5JphZqURU8W0hjYqiF4fhyuXnXqXdRZMigCBCwaGzNaIAWzlgAsmxxwhU1H1uO" \ + "PC4qUaPWDxURWwBwS/" \ + "g0sFv5AE6KC6KF8Anowg3yMtWjA6qg8HygEwOinpdrtjYsJgSEGGwEiqi3IwZ4lsKElRsRDvAs" \ + "YKwBXmtBk2tbC0ZY1ZXUuPMGGCqMuHp1XHDX3UoC+" \ + "UX9zoGRB19DemuhXgYyIYihW3GykLE9z3UsNEbUEMd2aKFXC69HrP5n+" \ + "s3moUYlwUXA9esFLvXI+" \ + "v7UVde0fwAADUYjKA2KsJGswMFQ6tBX4CAIDMl1N4z1MAAAAqGQCeNvSuBCZgBU9pQ29KYAJW8" \ + "HTXUsoUGSgzDWbMXKnQADGacdErikwJSAAAACmYiEERUkZJEKmgIFrG8BYsrgIqQQkpiNIQgoW" \ + "EOCLKMNX0b2NvYzdV1M6Yzr8gIogDm9ZCTfOJmZCo5RaToSEpnI50Do0jQ4mLuCwweuqE3iPz5" \ + "Ua1UONIruPhDfWhHowMniJMJIs+rNc+UjRohCNPrtYiRLGxmphYta2KPYNxwqnz3BAfSR8+" \ + "r5vo6BH7ITRKPJgMw55pYxAjtzXdYUtYOUlsOwEPjisZOGZGwknTpdBIFnEBgxW45kCnII+" \ + "JFgUAnjHMZcoLGIfUpWMggzitIVayXObeOyO49pfWMeKET0sAIgghvjs5ZCDUqW+gI3QB/" \ + "jrBrcgBIbaerbWY0DEmnJkAACsA6Q2wJcNud1FAB+MQ2PmwhiVkBQD+NuwxBjlBVy+" \ + "j7elt2GIKLKj1Mm3PhwmwYs6ws1cd1+" \ + "biUjKOMTcBAAAAREXFKFEWEQhFaTFaBJSYQCgUF6NoAYQiFSIQMQ2xsdpYbKyG6ZitgTqy2hoD" \ + "ajVtNXyZExYfJhjcFq4Lo/" \ + "YiBlAwCmXUtWSuDFcmjxkh3iCUEHPG47pIAklUBSbXQym55pSOaBgbYtG7XNUnRN7O7SOot4ek" \ + "OJjHycaH45rJ9el6TUKMIRfMNa9j7UEIugkBwgdrlTEJYRh3hgQCD4C8DjJkVrjgyFqiixylBs" \ + "4ImGVZRFhpqC6Kl5cybwTJ9CQi4zJOajqANYToGaqJAyAMYQv4ZU64M3TrKELHDYHpidhtgTtW" \ + "IXgAIGtHONjqd2vVRwIAxlAAoETnLWCKxCC6BXQ6AiD0RyBIkQkCFgCEsoTQB6ByY2wrkAmgTS" \ + "bzumo8bcity2wT4AC+" \ + "NmwiBbtMOg9jeF4DFuGDaSY1Xmw7gN4VKgAiIImU2zEBU1XprrhOwmYsdiAAAAAwRvhcDpfyKB" \ + "VwuVyGAEIxoZtvQjdJAcTFBOI+" \ + "EXeVSm5lLkogpKUeVnjkuq4Xx3XMmfD6cByzWonS8RjBIqCjq7DAgCNhMoEc36xgMsRFIsLKKP" \ + "ABCwEADqUu0oP3cKhH+AxA7IjucHffIRFg4FKYTBgeUo5LzMp8un6P4+DDIzMcV+" \ + "b6njKB2nfHGmABk0G6XlgwAHYKHxFdz/" \ + "fSgoOFxpQsgHJPDqlT364nHBXXEhvHAVOlBMObCAfXJsNEW8VNDAHgCtVHTXsYoyNh9EQPco1I" \ + "16PfOos28UYwlLBMdP7qnBTAba2w25wMGRTd9jxoW/" \ + "0LD8AQqtcXKXDqIYsjEgAgwm+MIGhxJJ1HG0DH6KIA2l5GaCOGjt+" \ + "N4E7qRgZAqCsiwrsQmRFAyDNdM8DlOOZeAF4HXLwLphj0eSFne64DTjFFZSLq46XIYvB0V0RVC" \ + "aQoInfFkLNLOVeVuE4sZjIHEgAAgJmVkBAkHRwAgZhAjJZKQGZHjBrIgPHZFvNMi8W08cHq7to" \ + "oZ1/" \ + "bnTplsEyvk2IyOF4CJZFLDV+" \ + "EEnDwpfTQqs3eexZGggtd4arG4ZXxXqLMWaquTbRJGLJIb4YBdM5L6XJGxOkrcoVDLqaKnKmDH" \ + "FHHLfYMDEjmhCAXr1TKkXkC1Bi0F5CwRsd0gHoU2T6dggaYyJ0ALvh+" \ + "3QpjABFwHKuw3wDovfPSTREWXBdhIe6UxALG+62jzGeQ04qVOSgC5EyahRCl+" \ + "cENZIZrYG6lHFm03BczCsVIO1pS9MNEh81Dp2s3pv/" \ + "aE7kJo25ZBeT4pml0gTAuF5abHe19EskfAfg+TDuffRgA0NuiuNGEEfjlebmjrE5hN/" \ + "cBZ0I8SQ9wOket640+" \ + "FiaiT859USQNpVU8JMgANjZ0ySTjIuACT2NDF10wIxAe4OmuqqEylFkZFVlGWZWZu4c4t1gptZ" \ + "WpvK6EUyUWkdlBJAAAkMYshYwaHkcRRTlLGAIx2lOCEiyniUAgaW6iss8SZevVSrr1uvIalYvh" \ + "V+H6XY/" \ + "5I0tvxe+4vskAmWsh9R3utFYXWR+yB+" \ + "K9uTrjhI3Oul6dRKRkRhSxyOgsOYim6ruDEAioHRctK3dGQkYZjRPLZiPqbsZ316ha6WtSm0mL" \ + "82GyBnN3TMBMVDHJwJEhOWK1MnARAw4K89uYGQDEI5TonZ5SXRgAOgpG4TLE0kk9YJHApSNw9j" \ + "A9nTBSHL4Sa/TBpHTpQupIdUSoM3JIT6NvLJm0AteV/" \ + "JgrQ2wAHWGSQLgVkDAMlPbqcuQyI5oG1LsXYDxvADWwilwj7ST6RgPMyhMDFAJcSDO8Y4LuqbU" \ + "6XS7ekbjouHmmAwCEugMAAIy/" \ + "DQA89rVFBlDIcLKVgAVc+UQCDBfOqdq7+" \ + "gkMSOAcdf9k1ZIZEcwrIRkwtMikFa8WOYBv3mUY9FGF9KsIEDiMDnUkmd2DmqD1wTAcH/" \ + "LhFQCs+STkJIWbf+" \ + "i1noRUk8LNa4RaNStrliIy7ZmnJZ6KioqqiuB2ZkveACC5lSARpEUIHQGhRMBlROiLPgrqdPjo" \ + "gN2JQIRiBgD4pYZpa2+KQ78cs9rMZCZTzafreLRabNW0cYBiiMDvAXwBBb2KvZavEyjk+" \ + "wIKepX0LF8nqJBv1ayhrChFxNjsdKRpGaG6jIzMVEZlDWUEIA3SdddcSYkTWjQFFAsYTACQKMZ" \ + "O1oqYJ2Tbmf3GdFJMw960FYemjWlBDQeOY1htBFARwV4cDQTZkZIIGjYUGgX/A/" \ + "21UBF4jsKEUhJBf2C5eioYLxB6aqyuijIjK7I6iojILMuK6pBFBuw74jgO7aoq9BbPbaS4GiAw" \ + "EgAAkGBJxEzEIEkEFoIhIKWQDAh3SuiC0KHdxMTFhAIxOLJV1wQMW8PE1vTl9eEZxxxhrmMbrx" \ + "/MS0UeK9xSwuXSMUXQbhgycx0Mp3Tw4ZUwD2ASItldCUp1C2ktjhwrXjxkUcoMBh08CsMStQIZ" \ + "nQDIizFhKHXoMJJDBhc8JcQT4vVOC2F1PQgHGRYmpBsjY2hA5nER5rjCVEuuFwGYXKPfDyEIWs" \ + "TohzgCHVjWMyIk478QRFHTYme1mKYgFluL1dZG1c6hYczUsDHf5dN1VmsPXhcHr2EeA5ODQBYp" \ + "wngdgH4bQXdo3T66QMAEwshJAACEwyJ1Obi+" \ + "zPXg03G8clwLdelg6Z2hrgBcj5UOXvn06XHleAsAAIAMAEkAdyjABX42lE6HPoLaZ1POlTwbGi" \ + "dCjaD2kZwr+" \ + "Q4VkQA4ogN7WiemLlehh56ejBAssocIkyQAAICQb4yDR8GCoeAR0CK0UITQRETANGFRyG7sC9x" \ + "lSkTSjRIQERG1ADaIvxETVfXU8GMKID6iYpp4BMIlKmBQuH4TQBuSNXhFbSCTSTLkwyMQwszMg" \ + "o54Z0R0hpQ0AZ0OnuIiw29a8ABmGNJkDg4e4YTeqbNokeh0xmnC5y0gpJ5Bj1BvPs11ZEmzJMb" \ + "z0G0v3YFhwKJTTyIc6kOJq+d5GOFVDZiECVpHgA7oATQoo97QCBCXHhkmB0wy5NMxJK8Pr+" \ + "QKcxw1bQzDtLG3KFZMBXkNC5lkEljwgBMUGFoAAMYagAkmIqDbIQCEgF8Ljhzp0RE2AwAAbSa/" \ + "ZIXJfHMB83oRAAAAafi2AExM4JxtN/" \ + "DESd8CtKpCoqoscAA2BjReBHlA7Dr+" \ + "HNEY0HgZmCD2ncGXE7prqIrKSFkZkRk1tW2cs2HvdjbD0cRyrq2T6rCxiakgAQAABKQRJAlmkk" \ + "JAKBAVF5cKZFFxSkwoTmgxIRGAZjEKtCglJipmY2uLrWKI/" \ + "WiYtn7Zy+" \ + "tWveCVuSbHdTBz5XjNdT2GZpl8RujoCNPweHEwMGGYzMzxLEshzDGZLImB15OIsBEYhvB5gsDM" \ + "EeAHiGJoD07S7dBLmNetmMcpkSOvJGRmuOA1gdKPvI4jMzw+" \ + "HT2BsUB1ZqLRRh6A4eARZq6FAN8SsJ4ajPLrBOBC54kTicW7LXMgsZANqjM90+ucyIQE+" \ + "H1DFQcMemZmH4mh9q0wc7wmU+kDx5UXMQVggMzpNHovTChAe/" \ + "EA6feMuge9d3rJlADvHSxK6Mpo0EYgjT+" \ + "lN4DLlZ4t1YUZJwBdgogOevvBVmv29EsXHcYEAIb4qOJcu5TvtVOAaYiBALGE0ykWcDNs+" \ + "VALJFMcp9F1Fk+" \ + "wwJmi2BpdTNSsUUrBItgBxJ6CAOCx8ID52yQZjpnXdV0cwlCG2DCmqaY9hnRcJ6cWSr8wmTurP" \ + "gl89cRIzKBFcO739eUTHFGDJuXU77q8qhII6hA6APgEAp6Qo7xFV495hVEj1zVWI5KLtbUbJY/" \ + "jlQdd0/" \ + "cyGKjgGIbvhVNcCwBs+" \ + "VDDUEANE7nG1o6SDAnUMLFTeVERQTLhCAcQMpQPwmGZLNoaoa4IXSj0kbpjcmOkR7ggzG8dydA" \ + "bPgixyHjNx8yaACT9fEbDNTnZ7ZPPa0FWTGfW00NVR9TbZtbUtiluzGZGAQTQv7AkEoSmiIebh" \ + "zhAIABlEHq60XRvRYQoY0q0EDOtDqfMqPamA79tpsPWCccc88ehQ6ti2Nk4cGTnt8UUtbWO9j5" \ + "WHEdmHh+7C9wIRcbudjS9CXjM7kaocWm3CfsmIKfZpvtHUVaURZ6uy8pxxLmzR5SEHNsXspeo+" \ + "DDzw4x9MaFSHm6tIyWiFempO9P2dtOMiWnTObTzx19O+Msvi/htb6NqsZ/" \ + "O3s7eMiJWj9dTFq5AEM9w6Lh/X076mm6aA9sCSIoJAHo13PxwXzBABvcU8c/" \ + "u7iQHUUTgzNM0Wo1dlmJRzDKZSAAAAAAwJwLO5a/4yl+5v++H209u/" \ + "94v13Pn59nDcvtSL21xLUpndfvy7VA5//" \ + "w4f27LOL795PYo+f7974+" \ + "SXupFFhk591zTOL79eH98nts4eItR07Oe9axnPeuZE0CZ27tPW1u3OzOKit91rOe+" \ + "1K7jFmVbZRyPx0AxaDGNyYnJp/7+eXL7yaB/9H/Pku1wvejpgenJsPvz+/" \ + "+mrSgjkevxerzVlbPImoxrYbKeY8BJsnrRY89KMmG12IoUvs3b7eanpycFiVHdouYWg8lsNmt1" \ + "FS+33+9zIiFVOq5jbU9nZ1MABYA2AQAAAAAAg0MAAAkAAACfd/skAWoWAKqz3++H+/" \ + "T0pGA6hrVD9XUrrc4zc3Nzc/r9sOZyIGq6bDnpYwKensDQ9y/" \ + "K37+cAFtbW3TudhWOtwZzlstb/X5/a6vf72/" \ + "x5+fm5nB6slBlZ3Fcha363d5ut7u3ni1rLoPf728l3KcK" + +namespace { + +const int kToggleCount = 4; +const int kNumChannels = 2; +const int kSampleRate = 44100; +const int kFramesPerBuffer = 882; // 10ms +const CefAudioHandler::ChannelLayout kChannelLayout = CEF_CHANNEL_LAYOUT_STEREO; + +const char kAudioOutputTestUrl[] = "http://tests/audiooutputtest"; +const char kAudioCloseBrowserTestUrl[] = "http://tests/audioclosebrowsertest"; +const char kAudioTogglePlaybackTestUrl[] = + "http://tests/audiotoggleplaybacktest"; + +const char kTestHtml[] = + "

TEST

"; + +const char kToggleTestHtml[] = + "

TEST

"; + +class AudioOutputTest : public ClientAppBrowser::Delegate { + public: + AudioOutputTest() {} + + void OnBeforeCommandLineProcessing( + CefRefPtr app, + CefRefPtr command_line) override { + // Allow media to autoplay without requiring user interaction. + command_line->AppendSwitchWithValue("autoplay-policy", + "no-user-gesture-required"); + } + + protected: + IMPLEMENT_REFCOUNTING(AudioOutputTest); +}; + +class AudioTestHandler : public TestHandler, public CefAudioHandler { + public: + AudioTestHandler() {} + + void SetupAudioTest(const std::string& testUrl) { + // Add the resource. + AddResource(testUrl, kTestHtml, "text/html"); + + // Create the browser. + CreateBrowser(testUrl); + + // Time out the test after a reasonable period of time. + SetTestTimeout(); + } + + void OnAfterCreated(CefRefPtr browser) override { + browser_ = browser; + TestHandler::OnAfterCreated(browser); + } + + CefRefPtr GetAudioHandler() override { return this; } + + bool GetAudioParameters(CefRefPtr browser, + CefAudioParameters& params) override { + EXPECT_TRUE(CefCurrentlyOn(TID_UI)); + params.channel_layout = kChannelLayout; + params.sample_rate = kSampleRate; + params.frames_per_buffer = kFramesPerBuffer; + got_audio_parameters_.yes(); + return true; + } + + void OnAudioStreamStarted(CefRefPtr browser, + const CefAudioParameters& params, + int channels) override { + EXPECT_FALSE(got_on_audio_stream_started_); + EXPECT_TRUE(got_audio_parameters_); + EXPECT_TRUE(browser_->IsSame(browser)); + EXPECT_EQ(channels, kNumChannels); + EXPECT_EQ(params.channel_layout, kChannelLayout); + EXPECT_EQ(params.sample_rate, kSampleRate); + EXPECT_EQ(params.frames_per_buffer, kFramesPerBuffer); + got_on_audio_stream_started_.yes(); + } + + void OnAudioStreamPacket(CefRefPtr browser, + const float** data, + int frames, + int64 pts) override { + EXPECT_TRUE(got_on_audio_stream_started_); + EXPECT_TRUE(browser_->IsSame(browser)); + EXPECT_EQ(frames, kFramesPerBuffer); + got_on_audio_stream_packet_.yes(); + } + + void OnAudioStreamStopped(CefRefPtr browser) override { + EXPECT_FALSE(got_on_audio_stream_stopped_); + EXPECT_TRUE(got_on_audio_stream_started_); + EXPECT_TRUE(browser_->IsSame(browser)); + EXPECT_TRUE(CefCurrentlyOn(TID_UI)); + got_on_audio_stream_stopped_.yes(); + DestroyTest(); + } + + void OnAudioStreamError(CefRefPtr browser, + const CefString& message) override { + LOG(WARNING) << "OnAudioStreamError: message = " << message << "."; + got_on_audio_stream_error_.yes(); + DestroyTest(); + } + + protected: + void DestroyTest() override { + browser_ = nullptr; + EXPECT_TRUE(got_audio_parameters_); + EXPECT_TRUE(got_on_audio_stream_started_); + EXPECT_TRUE(got_on_audio_stream_packet_); + EXPECT_TRUE(got_on_audio_stream_stopped_); + EXPECT_FALSE(got_on_audio_stream_error_); + TestHandler::DestroyTest(); + } + + CefRefPtr browser_; + TrackCallback got_on_audio_stream_started_; + TrackCallback got_on_audio_stream_packet_; + TrackCallback got_on_audio_stream_stopped_; + TrackCallback got_on_audio_stream_error_; + TrackCallback got_audio_parameters_; +}; + +// A common base class for audio output tests. +class AudioOutputTestHandler : public AudioTestHandler { + public: + AudioOutputTestHandler() {} + + void RunTest() override { + // Setup the resource. + SetupAudioTest(kAudioOutputTestUrl); + } + + void OnAudioStreamPacket(CefRefPtr browser, + const float** data, + int frames, + int64 pts) override { + if (!got_on_audio_stream_packet_.isSet()) { + browser->GetMainFrame()->ExecuteJavaScript( + "var ifr = document.getElementById(\"audio_output_frame\"); " + "ifr.parentNode.removeChild(ifr);", + CefString(), 0); + } + AudioTestHandler::OnAudioStreamPacket(browser, data, frames, pts); + } + + protected: + IMPLEMENT_REFCOUNTING(AudioOutputTestHandler); +}; + +class AudioCloseBrowserTest : public AudioTestHandler { + public: + AudioCloseBrowserTest() {} + + void RunTest() override { + // Setup the resource. + SetupAudioTest(kAudioCloseBrowserTestUrl); + } + + void OnBeforeClose(CefRefPtr browser) override { + EXPECT_FALSE(got_on_audio_stream_stopped_); + TestHandler::OnBeforeClose(browser); + } + + void OnAudioStreamPacket(CefRefPtr browser, + const float** data, + int frames, + int64 pts) override { + if (!got_on_audio_stream_packet_.isSet()) { + CloseBrowser(browser, true); + } + AudioTestHandler::OnAudioStreamPacket(browser, data, frames, pts); + } + + protected: + IMPLEMENT_REFCOUNTING(AudioCloseBrowserTest); +}; + +// kToggleCount represents the times the OnAudioStreamStarted and +// OnAudioStreamStopped should have been called. +// Both get called on playback start and end respectively. As well as when there +// is a period of silence for more than 2 seconds (see +// CefBrowserHostImpl::OnAudioStateChangedand and +// CefBrowserHostImpl::OnRecentlyAudibleTimerFired). The timeouts in kTestHtml +// represent play and pause timeouts. So for example it should play for 150ms +// and then pause for 1950ms. In this example OnAudioStreamStopped must not be +// called because it is below the 2 seconds threshold. So in this 10 timeouts +// there are exactly 3 which should trigger OnAudioStreamStarted and +// OnAudioStreamStopped together with the initial stream start and the final +// stream stop. And due to the need to test the toggle bounderies it results in +// this nearly 15 seconds test run. +class AudioTogglePlaybackTest : public AudioTestHandler { + public: + AudioTogglePlaybackTest() {} + + void RunTest() override { + // Add the resource. + AddResource(kAudioTogglePlaybackTestUrl, kToggleTestHtml, "text/html"); + + // Create the browser. + CreateBrowser(kAudioTogglePlaybackTestUrl); + + // Time out the test after a reasonable period of time. + SetTestTimeout(20000); + } + + void OnAudioStreamStarted(CefRefPtr browser, + const CefAudioParameters& params, + int channels) override { + EXPECT_TRUE(browser_->IsSame(browser)); + EXPECT_EQ(channels, kNumChannels); + EXPECT_EQ(params.channel_layout, kChannelLayout); + EXPECT_EQ(params.sample_rate, kSampleRate); + EXPECT_EQ(params.frames_per_buffer, kFramesPerBuffer); + got_on_audio_stream_started_.yes(); + start_count_++; + } + + void OnAudioStreamStopped(CefRefPtr browser) override { + EXPECT_EQ(start_count_, ++stop_count_); + if (stop_count_ == kToggleCount) + AudioTestHandler::OnAudioStreamStopped(browser); + } + + protected: + int start_count_ = 0; + int stop_count_ = 0; + IMPLEMENT_REFCOUNTING(AudioTogglePlaybackTest); +}; + +} // namespace + +// Test audio output callbacks called on valid threads. +TEST(AudioOutputTest, AudioOutputTest) { + CefRefPtr handler = new AudioOutputTestHandler(); + handler->ExecuteTest(); + ReleaseAndWaitForDestructor(handler); +} + +// Test audio stream stopped callback is called on browser close. +TEST(AudioOutputTest, AudioCloseBrowserTest) { + CefRefPtr handler = new AudioCloseBrowserTest(); + handler->ExecuteTest(); + ReleaseAndWaitForDestructor(handler); +} + +// Test audio stream starts/stops properly on certain time bounderies. +TEST(AudioOutputTest, AudioTogglePlaybackTest) { + CefRefPtr handler = new AudioTogglePlaybackTest(); + handler->ExecuteTest(); + ReleaseAndWaitForDestructor(handler); +} + +// Entry point for creating audio output test objects. +// Called from client_app_delegates.cc. +void CreateAudioOutputTests(ClientAppBrowser::DelegateSet& delegates) { + delegates.insert(new AudioOutputTest); +} diff --git a/tests/ceftests/client_app_delegates.cc b/tests/ceftests/client_app_delegates.cc index d2d8c2a67..a577a53be 100644 --- a/tests/ceftests/client_app_delegates.cc +++ b/tests/ceftests/client_app_delegates.cc @@ -9,6 +9,10 @@ using client::ClientAppBrowser; using client::ClientAppRenderer; void CreateBrowserDelegates(ClientAppBrowser::DelegateSet& delegates) { + // Bring in audio output tests. + extern void CreateAudioOutputTests(ClientAppBrowser::DelegateSet & delegates); + CreateAudioOutputTests(delegates); + // Bring in the Navigation tests. extern void CreateNavigationBrowserTests(ClientAppBrowser::DelegateSet & delegates); diff --git a/tools/cef_parser.py b/tools/cef_parser.py index 75f992ac0..dd80aedff 100644 --- a/tools/cef_parser.py +++ b/tools/cef_parser.py @@ -381,6 +381,7 @@ _simpletypes = { 'uint64': ['uint64', '0'], 'double': ['double', '0'], 'float': ['float', '0'], + 'float*': ['float*', 'NULL'], 'long': ['long', '0'], 'unsigned long': ['unsigned long', '0'], 'long long': ['long long', '0'], @@ -404,6 +405,7 @@ _simpletypes = { 'CefDraggableRegion': ['cef_draggable_region_t', 'CefDraggableRegion()'], 'CefThreadId': ['cef_thread_id_t', 'TID_UI'], 'CefTime': ['cef_time_t', 'CefTime()'], + 'CefAudioParameters': ['cef_audio_parameters_t', 'CefAudioParameters()'] }