diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d4ff4d6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,34 @@ +# How to become a contributor and submit your own code + +## Contributor License Agreements + +We'd love to accept your sample apps and patches! Before we can take them, we +have to jump a couple of legal hurdles. + +Please fill out either the individual or corporate Contributor License Agreement (CLA). + + * If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an [individual CLA] + (https://developers.google.com/open-source/cla/individual). + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate CLA] + (https://developers.google.com/open-source/cla/corporate). + +Follow either of the two links above to access the appropriate CLA and +instructions for how to sign and return it. Once we receive it, we'll be able to +accept your pull requests. + +## Contributing A Patch + +1. Submit an issue describing your proposed change to the repo in question. +1. The repo owner will respond to your issue promptly. +1. If your proposed change is accepted, and you haven't already done so, sign a + Contributor License Agreement (see details above). +1. Fork the desired repo, develop and test your code changes. +1. Ensure that your code adheres to the existing style in the sample to which + you are contributing. Refer to the + [Google C++ style guide] + (https://google.github.io/styleguide/cppguide.html) for the + recommended coding standards for this organization. +1. Ensure that your code has an appropriate set of unit tests which all pass. +1. Submit a pull request. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f0ca828 --- /dev/null +++ b/Makefile @@ -0,0 +1,71 @@ +# Copyright 2016 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +GRPC_SRC_PATH ?= ../grpc +GOOGLEAPIS_GENS_PATH ?= ../googleapis/gens +GOOGLEAPIS_API_CCS = $(shell find $(GOOGLEAPIS_GENS_PATH)/google/api \ + -name '*.pb.cc') +GOOGLEAPIS_RPC_CCS = $(shell find $(GOOGLEAPIS_GENS_PATH)/google/rpc \ + -name '*.pb.cc') + +GOOGLEAPIS_CCS = $(GOOGLEAPIS_API_CCS) $(GOOGLEAPIS_RPC_CCS) + +GOOGLEAPIS_ASSISTANT_CCS = embedded_assistant.pb.cc embedded_assistant.grpc.pb.cc + +HOST_SYSTEM = $(shell uname | cut -f 1 -d_) +SYSTEM ?= $(HOST_SYSTEM) +CXX = g++ +CPPFLAGS += -I/usr/local/include -pthread -I$(GOOGLEAPIS_GENS_PATH) \ + -I$(GRPC_SRC_PATH) +CXXFLAGS += -std=c++11 +# grpc_cronet is for JSON functions in gRPC library. +ifeq ($(SYSTEM),Darwin) +LDFLAGS += -L/usr/local/lib `pkg-config --libs grpc++ grpc` \ + -lgrpc++_reflection -lgrpc_cronet \ + -lprotobuf -lpthread -ldl -lcurl +else +LDFLAGS += -L/usr/local/lib `pkg-config --libs grpc++ grpc` \ + -lgrpc_cronet -Wl,--no-as-needed -lgrpc++_reflection \ + -Wl,--as-needed -lprotobuf -lpthread -ldl -lcurl +endif + +AUDIO_SRCS = +ifeq ($(SYSTEM),Linux) +AUDIO_SRCS += audio_input_alsa.cc audio_output_alsa.cc +LDFLAGS += `pkg-config --libs alsa` +endif + +.PHONY: all +all: run_assistant + +googleapis.ar: $(GOOGLEAPIS_CCS:.cc=.o) + ar r $@ $? + +run_assistant.o: $(GOOGLEAPIS_ASSISTANT_CCS:.cc=.h) + +run_assistant: run_assistant.o $(GOOGLEAPIS_ASSISTANT_CCS:.cc=.o) googleapis.ar \ + $(AUDIO_SRCS:.cc=.o) audio_input_file.o json_util.o service_account_util.o + $(CXX) $^ $(LDFLAGS) -o $@ + +json_util_test: json_util.o json_util_test.o + $(CXX) $^ $(LDFLAGS) -o $@ + +$(GOOGLEAPIS_ASSISTANT_CCS:.cc=.h) $(GOOGLEAPIS_ASSISTANT_CCS): embedded_assistant.proto + protoc --proto_path=.:$(GOOGLEAPIS_GENS_PATH)/..:/usr/local/include \ + --cpp_out=./ --grpc_out=./ --plugin=protoc-gen-grpc=/usr/local/bin/grpc_cpp_plugin $^ + +clean: + rm -f *.o run_assistant googleapis.ar \ + $(GOOGLEAPIS_CCS:.cc=.o) \ + $(GOOGLEAPIS_ASSISTANT_CCS) $(GOOGLEAPIS_ASSISTANT_CCS:.cc=.h) \ + $(GOOGLEAPIS_ASSISTANT_CCS:.cc=.o) diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc28795 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# gRPC C++ samples for Google Assistant SDK + +## Requirements + +This project is officially supported on Ubuntu 14.04. Other Linux distributions may be able to run +this sample. + +Refer to the [Assistant SDK documentation](https://developers.google.com/assistant/sdk/) for more information. + +## Setup instructions + +1. Remove previously installed Protobuf/gRPC/related packages and files. If you have not setup before, you can skip this step: +``` +sudo apt-get purge libc-ares-dev # https://github.com/grpc/grpc/pull/10706#issuecomment-302775038 +sudo apt-get purge libprotobuf-dev libprotoc-dev +sudo rm -rf /usr/local/bin/grpc_* /usr/local/bin/protoc \ + /usr/local/include/google/protobuf/ /usr/local/include/grpc/ /usr/local/include/grpc++/ \ + /usr/local/lib/libproto* /usr/local/lib/libgpr* /usr/local/lib/libgrpc* \ + /usr/local/lib/pkgconfig/protobuf* /usr/local/lib/pkgconfig/grpc* \ + /usr/local/share/grpc/ +``` + +2. Install dependencies +``` +sudo apt-get install autoconf automake libtool build-essential curl unzip +sudo apt-get install libasound2-dev # For ALSA sound output +sudo apt-get install libcurl4-openssl-dev # CURL development library +``` + +3. Build Protocol Buffer, gRPC and Google APIs +``` +git clone -b $(curl -L https://grpc.io/release) https://github.com/grpc/grpc +cd grpc/ +git submodule update --init + +cd third_party/protobuf +./autogen.sh && ./configure && make +sudo make install +sudo ldconfig + +cd ../../ +make +sudo make install +sudo ldconfig + +cd ../ +git clone https://github.com/googleapis/googleapis.git +cd googleapis/ +make LANGUAGE=cpp +``` + +4. Make sure you setup env variable `$GOOGLEAPIS_GENS_PATH` +``` +export GOOGLEAPIS_GENS_PATH=./gens +``` + +5. Clone and build assistant-grpc +``` +cd ../ +git clone https://github.com/googlebowang/assistant-grpc.git + +cd assistant-grpc/ +make run_assistant +``` + +6. Get credentials file. It can be either an end-user's credentials, or a service account's credentials. + +For end-user's credentials: + +* Download the client secret json file from Google Cloud Platform Console following [these instructions](https://developers.google.com/assistant/sdk/develop/python/config-dev-project-and-account) +* Move it in this folder and rename it to `client_secret.json` +* run `get_credentials.sh` in this folder. It will create the file `credentials.json`. + +For service account's credentials in Google Cloud Platform Console: + +* Open **API Manager -> Credentials** +* Click **Create credentials -> Service account key**. Choose key type **JSON**, and click **Create** +* The service account's credentials JSON file will be downloaded. Notice that this JSON file can only be downloaded once, so keep it for future reference. + +7. Start `run_assistant` +``` +./run_assistant --audio_input ./resources/switch_to_channel_5.raw --credentials_file ./credentials.json --credentials_type USER_ACCOUNT +./run_assistant --audio_input ./resources/weather_in_mountain_view.raw --credentials_file --credentials_type SERVICE_ACCOUNT +# On Linux workstation, you can also use ALSA audio input: +./run_assistant --audio_input ALSA_INPUT --credentials_file ./credentials.json --credentials_type USER_ACCOUNT +``` + +Default Assistant gRPC API endpoint is embeddedassistant.googleapis.com. If you want to test with a custom Assistant gRPC API endpoint, you can pass an extra "--api_endpoint CUSTOM_API_ENDPOINT" to run_assistant. diff --git a/assistant_config.h b/assistant_config.h new file mode 100644 index 0000000..4b2c698 --- /dev/null +++ b/assistant_config.h @@ -0,0 +1,23 @@ +/* +Copyright 2017 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef ASSISTANT_CONFIG_H +#define ASSISTANT_CONFIG_H + +// This is the endpoint to send gRPC data. +#define ASSISTANT_ENDPOINT "embeddedassistant.googleapis.com" + +#endif diff --git a/audio_input.h b/audio_input.h new file mode 100644 index 0000000..e8f5d40 --- /dev/null +++ b/audio_input.h @@ -0,0 +1,102 @@ +/* +Copyright 2017 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef AUDIO_INPUT_H +#define AUDIO_INPUT_H + +#include +#include +#include +#include +#include +#include + +// Base class for audio input. Input data should be mono, s16_le, 16000kz. +// This class uses a separate thread to send audio data to listeners. +class AudioInput { + public: + virtual ~AudioInput() {} + + // Listeners might be called in different thread. + void AddDataListener( + std::function>)> + listener) { + data_listeners_.push_back(listener); + } + void AddStopListener(std::function listener) { + stop_listeners_.push_back(listener); + } + + // This thread should: + // 1. Initialize necessary resources; + // 2. If |is_running_| is still true, keep sending audio data; + // 3. Finalize necessary resources. + // 4. Call |OnStop|. + virtual std::unique_ptr GetBackgroundThread() = 0; + + // Asynchronously starts audio input. Starts internal thread to send audio. + void Start() { + std::unique_lock lock(is_running_mutex_); + if (is_running_) { + return; + } + + send_thread_ = std::move(GetBackgroundThread()); + is_running_ = true; + return; + } + + // Synchronously stops audio input. + void Stop() { + std::unique_lock lock(is_running_mutex_); + if (!is_running_) { + return; + } + is_running_ = false; + // |send_thread_| might have finished. + if (send_thread_->joinable()) { + send_thread_->join(); + } + } + + // Whether audio input is being sent to listeners. + bool IsRunning() { + std::unique_lock lock(is_running_mutex_); + return is_running_; + } + + protected: + // Function to call when audio input is stopped. + void OnStop() { + for (auto& stop_listener : stop_listeners_) { + stop_listener(); + } + } + + // Listeners which will be called when audio input data arrives. + std::vector>)>> + data_listeners_; + + // Whether audio input is being sent to listeners. + bool is_running_ = false; + + private: + std::vector> stop_listeners_; + std::mutex is_running_mutex_; + std::unique_ptr send_thread_; +}; + +#endif diff --git a/audio_input_alsa.cc b/audio_input_alsa.cc new file mode 100644 index 0000000..16a2b7f --- /dev/null +++ b/audio_input_alsa.cc @@ -0,0 +1,104 @@ +/* +Copyright 2017 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "audio_input_alsa.h" + +#include + +#include + +std::unique_ptr AudioInputALSA::GetBackgroundThread() { + return std::unique_ptr(new std::thread([this]() { + // Initialize. + snd_pcm_t* pcm_handle; + int pcm_open_ret = snd_pcm_open(&pcm_handle, "default", SND_PCM_STREAM_CAPTURE, 0); + if (pcm_open_ret < 0) { + std::cerr << "AudioInputALSA snd_pcm_open returned " << pcm_open_ret << std::endl; + return; + } + int pcm_nonblock_ret = snd_pcm_nonblock(pcm_handle, SND_PCM_NONBLOCK); + if (pcm_nonblock_ret < 0) { + std::cerr << "AudioInputALSA snd_pcm_nonblock returned " << pcm_nonblock_ret << std::endl; + return; + } + snd_pcm_hw_params_t* pcm_params; + int malloc_param_ret = snd_pcm_hw_params_malloc(&pcm_params); + if (malloc_param_ret < 0) { + std::cerr << "AudioInputALSA snd_pcm_hw_params_malloc returned " << malloc_param_ret + << std::endl; + return; + } + snd_pcm_hw_params_any(pcm_handle, pcm_params); + int set_param_ret = + snd_pcm_hw_params_set_access(pcm_handle, pcm_params, SND_PCM_ACCESS_RW_INTERLEAVED); + if (set_param_ret < 0) { + std::cerr << "AudioInputALSA snd_pcm_hw_params_set_access returned " << set_param_ret + << std::endl; + return; + } + set_param_ret = + snd_pcm_hw_params_set_format(pcm_handle, pcm_params, SND_PCM_FORMAT_S16_LE); + if (set_param_ret < 0) { + std::cerr << "AudioInputALSA snd_pcm_hw_params_set_format returned " << set_param_ret + << std::endl; + return; + } + set_param_ret = + snd_pcm_hw_params_set_channels(pcm_handle, pcm_params, 1); + if (set_param_ret < 0) { + std::cerr << "AudioInputALSA snd_pcm_hw_params_set_channels returned " << set_param_ret + << std::endl; + return; + } + unsigned int rate = 16000; + set_param_ret = + snd_pcm_hw_params_set_rate_near(pcm_handle, pcm_params, &rate, nullptr); + if (set_param_ret < 0) { + std::cerr << "AudioInputALSA snd_pcm_hw_params_set_rate_near returned " << set_param_ret + << std::endl; + return; + } + set_param_ret = snd_pcm_hw_params(pcm_handle, pcm_params); + if (set_param_ret < 0) { + std::cerr << "AudioInputALSA snd_pcm_hw_params returned " << set_param_ret << std::endl; + return; + } + snd_pcm_hw_params_free(pcm_params); + + while (is_running_) { + std::shared_ptr> audio_data( + new std::vector(kFramesPerPacket * kBytesPerFrame)); + int pcm_read_ret = snd_pcm_readi(pcm_handle, &(*audio_data.get())[0], kFramesPerPacket); + if (pcm_read_ret == -EAGAIN) { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } else if (pcm_read_ret < 0) { + std::cerr << "AudioInputALSA snd_pcm_readi returned " << pcm_read_ret << std::endl; + break; + } else if (pcm_read_ret > 0) { + audio_data->resize(kBytesPerFrame * pcm_read_ret); + for (auto& listener : data_listeners_) { + listener(audio_data); + } + } + } + + // Finalize. + snd_pcm_close(pcm_handle); + + // Call |OnStop|. + OnStop(); + })); +} diff --git a/audio_input_alsa.h b/audio_input_alsa.h new file mode 100644 index 0000000..68f9180 --- /dev/null +++ b/audio_input_alsa.h @@ -0,0 +1,30 @@ +/* +Copyright 2017 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "audio_input.h" + +class AudioInputALSA : public AudioInput { + public: + ~AudioInputALSA() override {} + + virtual std::unique_ptr GetBackgroundThread() override; + + private: + // For 16000Hz, it's about 0.1 second. + static constexpr int kFramesPerPacket = 1600; + // 1 channel, S16LE, so 2 bytes each frame. + static constexpr int kBytesPerFrame = 2; +}; diff --git a/audio_input_file.cc b/audio_input_file.cc new file mode 100644 index 0000000..2e331e4 --- /dev/null +++ b/audio_input_file.cc @@ -0,0 +1,55 @@ +/* +Copyright 2017 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "audio_input_file.h" + +#include +#include + +std::unique_ptr AudioInputFile::GetBackgroundThread() { + return std::unique_ptr(new std::thread([this]() { + // Initialize. + std::ifstream file_stream(file_path_); + if (!file_stream) { + std::cerr << "AudioInputFile cannot open file " << file_path_ << std::endl; + return; + } + + const size_t chunk_size = 20 * 1024; // 20KB + std::shared_ptr> chunk( + new std::vector); + chunk->resize(chunk_size); + while (is_running_) { + // Read another chunk from the file. + std::streamsize bytes_read = + file_stream.rdbuf()->sgetn((char*)&(*chunk.get())[0], chunk->size()); + if (bytes_read > 0) { + chunk->resize(bytes_read); + for (auto& listener : data_listeners_) { + listener(chunk); + } + } + if (bytes_read < chunk->size()) { + break; + } + // Wait a second before writing the next chunk. + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + // Call |OnStop|. + OnStop(); + })); +} diff --git a/audio_input_file.h b/audio_input_file.h new file mode 100644 index 0000000..86a4466 --- /dev/null +++ b/audio_input_file.h @@ -0,0 +1,28 @@ +/* +Copyright 2017 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "audio_input.h" + +class AudioInputFile : public AudioInput { + public: + AudioInputFile(const std::string& file_path): file_path_(file_path) {} + ~AudioInputFile() override {} + + virtual std::unique_ptr GetBackgroundThread() override; + + private: + const std::string file_path_; +}; diff --git a/audio_output_alsa.cc b/audio_output_alsa.cc new file mode 100644 index 0000000..c5cb51b --- /dev/null +++ b/audio_output_alsa.cc @@ -0,0 +1,128 @@ +/* +Copyright 2017 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "audio_output_alsa.h" + +#include + +#include + +bool AudioOutputALSA::Start() { + std::unique_lock lock(isRunningMutex); + + if (isRunning) { + return true; + } + + snd_pcm_t* pcm_handle; + int pcm_open_ret = snd_pcm_open(&pcm_handle, "default", SND_PCM_STREAM_PLAYBACK, 0); + if (pcm_open_ret < 0) { + std::cerr << "AudioOutputALSA snd_pcm_open returned " << pcm_open_ret << std::endl; + return false; + } + snd_pcm_hw_params_t* pcm_params; + int malloc_param_ret = snd_pcm_hw_params_malloc(&pcm_params); + if (malloc_param_ret < 0) { + std::cerr << "AudioOutputALSA snd_pcm_hw_params_malloc returned " << malloc_param_ret + << std::endl; + return false; + } + snd_pcm_hw_params_any(pcm_handle, pcm_params); + int set_param_ret = + snd_pcm_hw_params_set_access(pcm_handle, pcm_params, SND_PCM_ACCESS_RW_INTERLEAVED); + if (set_param_ret < 0) { + std::cerr << "AudioOutputALSA snd_pcm_hw_params_set_access returned " << set_param_ret + << std::endl; + return false; + } + set_param_ret = + snd_pcm_hw_params_set_format(pcm_handle, pcm_params, SND_PCM_FORMAT_S16_LE); + if (set_param_ret < 0) { + std::cerr << "AudioOutputALSA snd_pcm_hw_params_set_format returned " << set_param_ret + << std::endl; + return false; + } + set_param_ret = + snd_pcm_hw_params_set_channels(pcm_handle, pcm_params, 1); + if (set_param_ret < 0) { + std::cerr << "AudioOutputALSA snd_pcm_hw_params_set_channels returned " << set_param_ret + << std::endl; + return false; + } + unsigned int rate = 16000; + set_param_ret = + snd_pcm_hw_params_set_rate_near(pcm_handle, pcm_params, &rate, nullptr); + if (set_param_ret < 0) { + std::cerr << "AudioOutputALSA snd_pcm_hw_params_set_rate_near returned " << set_param_ret + << std::endl; + return false; + } + set_param_ret = snd_pcm_hw_params(pcm_handle, pcm_params); + if (set_param_ret < 0) { + std::cerr << "AudioOutputALSA snd_pcm_hw_params returned " << set_param_ret << std::endl; + return false; + } + snd_pcm_hw_params_free(pcm_params); + + isRunning = true; + alsaThread.reset(new std::thread([this, pcm_handle]() { + while (isRunning) { + std::unique_lock lock(audioDataMutex); + while (audioData.size() == 0 && isRunning) { + audioDataCv.wait_for(lock, std::chrono::milliseconds(100)); + } + if (!isRunning) { + break; + } + + std::shared_ptr> data = audioData[0]; + audioData.erase(audioData.begin()); + int frames = data->size() / 2; // 1 channel, S16LE, so 2 bytes each frame. + int pcm_write_ret = snd_pcm_writei(pcm_handle, &(*data.get())[0], frames); + if (pcm_write_ret < 0) { + int pcm_recover_ret = snd_pcm_recover(pcm_handle, pcm_write_ret, 0); + if (pcm_recover_ret < 0) { + std::cerr << "AudioOutputALSA snd_pcm_recover returns " << pcm_recover_ret << std::endl; + break; + } + } + } + // Wait for all data to be consumed. + snd_pcm_drain(pcm_handle); + snd_pcm_close(pcm_handle); + })); + std::cout << "AudioOutputALSA Start() succeeded" << std::endl; + return true; +} + +void AudioOutputALSA::Stop() { + std::unique_lock lick(isRunningMutex); + + if (!isRunning) { + return; + } + + isRunning = false; + alsaThread->join(); + alsaThread.reset(nullptr); + std::cout << "AudioOutputALSA Stop() succeeded" << std::endl; +} + +void AudioOutputALSA::Send(std::shared_ptr> data) { + std::unique_lock lock(audioDataMutex); + audioData.push_back(data); + audioDataCv.notify_one(); +} diff --git a/audio_output_alsa.h b/audio_output_alsa.h new file mode 100644 index 0000000..4bacc98 --- /dev/null +++ b/audio_output_alsa.h @@ -0,0 +1,40 @@ +/* +Copyright 2017 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include +#include +#include +#include + +// Audio output using ALSA. +class AudioOutputALSA { + public: + bool Start(); + + void Stop(); + + void Send(std::shared_ptr> data); + + friend void fill_audio(void* userdata, unsigned char* stream, int len); + + private: + std::vector>> audioData; + std::mutex audioDataMutex; + std::condition_variable audioDataCv; + std::unique_ptr alsaThread; + bool isRunning = false; + std::mutex isRunningMutex; +}; diff --git a/embedded_assistant.proto b/embedded_assistant.proto new file mode 100644 index 0000000..587ff4d --- /dev/null +++ b/embedded_assistant.proto @@ -0,0 +1,401 @@ +syntax = "proto3"; + +package google.assistant.embedded.v1alpha1; + +option java_multiple_files = true; +option java_outer_classname = "AssistantProto"; +option java_package = "com.google.assistant.embedded.v1alpha1"; + +import "google/api/annotations.proto"; +import "google/rpc/status.proto"; + +// Service that implements Google Assistant API. +service EmbeddedAssistant { + // Initiates or continues a conversation with the embedded assistant service. + // Each call performs one round-trip, sending an audio request to the service + // and receiving the audio response. Uses bidirectional streaming to receive + // results, such as the `END_OF_UTTERANCE` event, while sending audio. + // + // A conversation is one or more gRPC connections, each consisting of several + // streamed requests and responses. + // For example, the user says *Add to my shopping list* and the assistant + // responds *What do you want to add?*. The sequence of streamed requests and + // responses in the first gRPC message could be: + // + // * ConverseRequest.config + // * ConverseRequest.audio_in + // * ConverseResponse.interim_spoken_request_text "add" + // * ConverseRequest.audio_in + // * ConverseResponse.interim_spoken_request_text "add to my" + // * ConverseRequest.audio_in + // * ConverseResponse.interim_spoken_request_text "add to my shopping" + // * ConverseRequest.audio_in + // * ConverseResponse.event_type.END_OF_UTTERANCE + // * ConverseResponse.result.microphone_mode.DIALOG_FOLLOW_ON + // * ConverseResponse.audio_out + // * ConverseResponse.audio_out + // * ConverseResponse.audio_out + // + // Note that interim_spoken_request_text is returned asynchronously, which + // means that it could arrive in any order before the END_OF_UTTERANCE event + // occurs. + // + // The user then says *bagels* and the assistant responds + // *OK, I've added bagels to your shopping list*. This is sent as another gRPC + // connection call to the `Converse` method, again with streamed requests and + // responses, such as: + // + // * ConverseRequest.config + // * ConverseRequest.audio_in + // * ConverseRequest.audio_in + // * ConverseRequest.audio_in + // * ConverseResponse.event_type.END_OF_UTTERANCE + // * ConverseResponse.result.microphone_mode.CLOSE_MICROPHONE + // * ConverseResponse.screen_out + // * ConverseResponse.audio_out + // * ConverseResponse.audio_out + // * ConverseResponse.audio_out + // * ConverseResponse.audio_out + // + // Although the precise order of responses is not guaranteed, sequential + // ConverseResponse.audio_out messages will always contain sequential portions + // of audio. + rpc Converse(stream ConverseRequest) returns (stream ConverseResponse); +} + +// Specifies how to process the `ConverseRequest` messages. +message ConverseConfig { + // *Required* Specifies how to process the subsequent incoming audio. + AudioInConfig audio_in_config = 1; + + // *Required* Specifies how to format the audio that will be returned. + AudioOutConfig audio_out_config = 2; + + // *Required* Represents the current dialog state. + ConverseState converse_state = 3; + + // Device configuration that uniquely identifies a specific device. + DeviceConfig device_config = 4; +} + +// Specifies how to process the `audio_in` data that will be provided in +// subsequent requests. For recommended settings, see the Google Assistant SDK +// [best practices](https://developers.google.com/assistant/sdk/best-practices/audio). +message AudioInConfig { + // Audio encoding of the data sent in the audio message. + // Audio must be one-channel (mono). The only language supported is "en-US". + enum Encoding { + // Not specified. Will return result [google.rpc.Code.INVALID_ARGUMENT][]. + ENCODING_UNSPECIFIED = 0; + + // Uncompressed 16-bit signed little-endian samples (Linear PCM). + // This encoding includes no header, only the raw audio bytes. + LINEAR16 = 1; + + // [`FLAC`](https://xiph.org/flac/documentation.html) (Free Lossless Audio + // Codec) is the recommended encoding because it is + // lossless--therefore recognition is not compromised--and + // requires only about half the bandwidth of `LINEAR16`. This encoding + // includes the `FLAC` stream header followed by audio data. It supports + // 16-bit and 24-bit samples, however, not all fields in `STREAMINFO` are + // supported. + FLAC = 2; + } + + // *Required* Encoding of audio data sent in all `audio_in` messages. + Encoding encoding = 1; + + // *Required* Sample rate (in Hertz) of the audio data sent in all `audio_in` + // messages. Valid values are from 16000-24000, but 16000 is optimal. + // For best results, set the sampling rate of the audio source to 16000 Hz. + // If that's not possible, use the native sample rate of the audio source + // (instead of re-sampling). + int32 sample_rate_hertz = 2; +} + +// Specifies the desired format for the server to use when it returns +// `audio_out` messages. +message AudioOutConfig { + // Audio encoding of the data returned in the audio message. All encodings are + // raw audio bytes with no header, except as indicated below. + enum Encoding { + // Not specified. Will return result [google.rpc.Code.INVALID_ARGUMENT][]. + ENCODING_UNSPECIFIED = 0; + + // Uncompressed 16-bit signed little-endian samples (Linear PCM). + LINEAR16 = 1; + + // MP3 audio encoding. The sample rate is encoded in the payload. + MP3 = 2; + + // Opus-encoded audio wrapped in an ogg container. The result will be a + // file which can be played natively on Android and in some browsers (such + // as Chrome). The quality of the encoding is considerably higher than MP3 + // while using the same bitrate. The sample rate is encoded in the payload. + OPUS_IN_OGG = 3; + } + + // *Required* The encoding of audio data to be returned in all `audio_out` + // messages. + Encoding encoding = 1; + + // *Required* The sample rate in Hertz of the audio data returned in + // `audio_out` messages. Valid values are: 16000-24000. + int32 sample_rate_hertz = 2; + + // *Required* Current volume setting of the device's audio output. + // Valid values are 1 to 100 (corresponding to 1% to 100%). + int32 volume_percentage = 3; +} + +// Provides information about the current dialog state. +message ConverseState { + // *Required* The `conversation_state` value returned in the prior + // `ConverseResponse`. Omit (do not set the field) if there was no prior + // `ConverseResponse`. If there was a prior `ConverseResponse`, do not omit + // this field; doing so will end that conversation (and this new request will + // start a new conversation). + bytes conversation_state = 1; + + // *Optional* A way to provide context to assist the Assistant. + ConverseContext context = 3; + + // Specifies user is in SIGN_OUT mode. That means user credential is not + // available on the client. Note that client still needs to provide valid + // credentials to do server to server authentication (i.e., service account). + bool is_signed_out_mode = 4; +} + +// The audio containing the Assistant's response to the query. Sequential chunks +// of audio data are received in sequential `ConverseResponse` messages. +message AudioOut { + // *Output-only* The audio data containing the assistant's response to the + // query. Sequential chunks of audio data are received in sequential + // `ConverseResponse` messages. + bytes audio_data = 1; +} + +// The visual containing the Assistant's response to the query. Contains the +// whole visual output. +message ScreenOut { + // Possible formats of the visual data. + enum Format { + // No format specified. + FORMAT_UNSPECIFIED = 0; + + // Data will contain a fully-formed HTML5 layout encoded in UTF-8, e.g. + // "
...
". It is intended to be rendered + // along with the audio response. Note that HTML5 doctype should be included + // in the actual HTML data. + HTML = 1; + } + + // *Output-only* The format of the provided visual data. + Format format = 1; + + // *Output-only* The raw visual data to be displayed as the result of the + // Assistant query. + bytes data = 2; +} + +// The semantic result for the user's spoken query. Multiple of these messages +// could be received, for example one containing the recognized transcript in +// spoken_request_text followed by one containing the semantics of the response, +// i.e. containing the relevant data among conversation_state, microphone_mode, +// and volume_percentage. +message ConverseResult { + // *Output-only* The recognized transcript of what the user said. + string spoken_request_text = 1; + + // *Output-only* The text of the assistant's spoken response. This is only + // returned for an IFTTT action. + string spoken_response_text = 2; + + // *Output-only* State information for subsequent `ConverseRequest`. This + // value should be saved in the client and returned in the + // `conversation_state` with the next `ConverseRequest`. (The client does not + // need to interpret or otherwise use this value.) There is no need to save + // this information across device restarts. + bytes conversation_state = 3; + + // Possible states of the microphone after a `Converse` RPC completes. + enum MicrophoneMode { + // No mode specified. + MICROPHONE_MODE_UNSPECIFIED = 0; + + // The service is not expecting a follow-on question from the user. + // The microphone should remain off until the user re-activates it. + CLOSE_MICROPHONE = 1; + + // The service is expecting a follow-on question from the user. The + // microphone should be re-opened when the `AudioOut` playback completes + // (by starting a new `Converse` RPC call to send the new audio). + DIALOG_FOLLOW_ON = 2; + } + + // *Output-only* Specifies the mode of the microphone after this `Converse` + // RPC is processed. + MicrophoneMode microphone_mode = 4; + + // *Output-only* Updated volume level. The value will be 0 or omitted + // (indicating no change) unless a voice command such as "Increase the volume" + // or "Set volume level 4" was recognized, in which case the value will be + // between 1 and 100 (corresponding to the new volume level of 1% to 100%). + // Typically, a client should use this volume level when playing the + // `audio_out` data, and retain this value as the current volume level and + // supply it in the `AudioOutConfig` of the next `ConverseRequest`. (Some + // clients may also implement other ways to allow the current volume level to + // be changed, for example, by providing a knob that the user can turn.) + int32 volume_percentage = 5; +} + +// The top-level message sent by the client. Clients must send at least two, and +// typically numerous `ConverseRequest` messages. The first message must +// contain a `config` message and must not contain `audio_in` data. All +// subsequent messages must contain `audio_in` data and must not contain a +// `config` message. +message ConverseRequest { + + // Exactly one of these fields must be specified in each `ConverseRequest`. + oneof converse_request { + // The `config` message provides information to the recognizer that + // specifies how to process the request. + // The first `ConverseRequest` message must contain a `config` message. + ConverseConfig config = 1; + + // The audio data to be recognized. Sequential chunks of audio data are sent + // in sequential `ConverseRequest` messages. The first `ConverseRequest` + // message must not contain `audio_in` data and all subsequent + // `ConverseRequest` messages must contain `audio_in` data. The audio bytes + // must be encoded as specified in `AudioInConfig`. + // Audio must be sent at approximately real-time (16000 samples per second). + // An error will be returned if audio is sent significantly faster or + // slower. + bytes audio_in = 2; + } +} + +// The top-level message received by the client. A series of one or more +// `ConverseResponse` messages are streamed back to the client. +message ConverseResponse { + // Indicates the type of event. + enum EventType { + // No event specified. + EVENT_TYPE_UNSPECIFIED = 0; + + // This event indicates that the server has detected the end of the user's + // speech utterance and expects no additional speech. Therefore, the server + // will not process additional audio (although it may subsequently return + // additional results). The client should stop sending additional audio + // data, half-close the gRPC connection, and wait for any additional results + // until the server closes the gRPC connection. + END_OF_UTTERANCE = 1; + } + + // Exactly one of these fields will be populated in each `ConverseResponse`. + oneof converse_response { + // *Output-only* If set, returns a [google.rpc.Status][] message that + // specifies the error for the operation. + // If an error occurs during processing, this message will be set and there + // will be no further messages sent. + google.rpc.Status error = 1; + + // *Output-only* Indicates the type of event. + EventType event_type = 2; + + // *Output-only* The audio containing the Assistant's response to the query. + AudioOut audio_out = 3; + + // *Output-only* The visual containing the Assistant's response to the + // query. + ScreenOut screen_out = 8; + + // *Output-only* Contains the action triggered by the query with the + // appropriate payloads and semantic parsing. + DeviceAction device_action = 9; + + // *Output-only* The interim recognition result that corresponds to the + // audio currently being processed. The text returned is purely + // informational and will not be issued as commands to the Assistant. Once + // the user's spoken query has been fully recognized, the final complete + // utterance issued to the Assistant is returned in 'spoken_request_text' in + // [ConverseResult](google.assistant.embedded.v1alpha1.ConverseResult) + InterimSpokenRequestText interim_spoken_request_text = 6; + + // *Output-only* The final semantic result for the user's spoken query. + ConverseResult result = 5; + } +} + +// The interim speech recognition text as the user is speaking. Contains the +// estimated transcription of what the user has spoken thus far from the input +// audio. It can be used to display the current guess of the user's query. +message InterimSpokenRequestText { + // *Output-only* Transcript text representing the words that the user spoke. + string transcript = 1; + + // *Output-only* An estimate of the likelihood that the Assistant will not + // change its guess about this result. Values range from 0.0 (completely + // unstable) to 1.0 (completely stable). The default of 0.0 is a sentinel + // value indicating `stability` was not set. + double stability = 2; +} + +// The identification information for devices integrated with the Assistant. +// These fields should be populated for any queries sent from 3P devices. +message DeviceConfig { + // *Required* Unique identifier for the device. Example: DBCDW098234. This + // MUST match the device_id returned from device registration. This device_id + // is used matched against the user's registered devices to lookup the + // supported traits and capabilities of this device. + string device_id = 1; + + // *Optional* The model of this device as registered in the Device Model + // Registration API. This is only required for syndication partners. + string device_model_id = 3; +} + +// Context provided to the Assistant to enhance its capabilities for the +// Converse API. +message ConverseContext { + // *Optional* A list of strings containing words and phrases "hints" so that + // the speech recognition is more likely to recognize them. This can be used + // to improve the accuracy for specific words and phrases, such as words + // or commands that the user is likely to say (for example, choices displayed + // on-screen). + repeated string hinted_phrases = 1; + + // *Optional* Custom context from the device passed along to 3P fulfillment + // service. This can provide information about the device's current state. + // This data is opaque to the Assistant and is defined by the 3P provider, + // such as a JSON string or binary data. For passing binary data, the data + // should be base64 encoded. + string third_party_context = 2; +} + +// The response returned to the device if any 3P Custom Device Grammar is +// triggered. The 3P Custom Device Grammar is enabled through the specific +// [DeviceConfig](google.assistant.embedded.v1alpha1.DeviceConfig) provided by +// this device, and should be handled appropriately. For example, a 3P device +// which supports the customized query "do a dance" would receive a DeviceAction +// with action_type: "device_control" and a JSON payload containing the +// semantics of the request. +message DeviceAction { + // The type of action to be executed on the device. + enum ActionType { + // Unknown action type. + ACTION_TYPE_UNSPECIFIED = 0; + + // 3P custom device action. + DEVICE_CONTROL = 1; + } + + // What type of action this DeviceAction contains. + ActionType action_type = 1; + + // JSON containing the device control response generated from the triggered 3P + // Custom Device Grammar. The format is given by the [action.devices.EXECUTE]( + // https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute) + // request type. + string device_control_json = 2; +} diff --git a/get_credentials.sh b/get_credentials.sh new file mode 100755 index 0000000..b196129 --- /dev/null +++ b/get_credentials.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# The input of this file is client_secret json file. Please download it to this folder and rename it to client_secret.json +# If succeeded, it will output credentials.json, which can be used with run_assistant. + +CLIENT_SECRET_FILE="client_secret.json" +if ! [ -f $CLIENT_SECRET_FILE ] ; then + echo "Please download client_secret json file to this folder, and change its name to '$CLIENT_SECRET_FILE'." + exit 1 +fi + +CLIENT_ID=`cat $CLIENT_SECRET_FILE | grep -o -e '"client_id":"[^"]*"' | sed -e 's/.*"client_id":"\([^"]*\)".*/\1/g'` +if [ -z $CLIENT_ID ]; then + echo "No client_id found in client secret file $CLIENT_SECRET_FILE. Exiting." + exit 1 +fi +echo "Client id: $CLIENT_ID" +CLIENT_SECRET=`cat $CLIENT_SECRET_FILE | grep -o -e '"client_secret":"[^"]*"' | sed -e 's/.*"client_secret":"\([^"]*\)".*/\1/g'` +if [ -z $CLIENT_SECRET ]; then + echo "No client_secret found in client secret file $CLIENT_SECRET_FILE. Exiting." + exit 1 +fi +echo "Client secret: $CLIENT_SECRET" +echo "" + +echo "Please go to the following link to authorize, and copy back the code here:" +echo "https://accounts.google.com/o/oauth2/v2/auth?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fassistant-sdk-prototype&access_type=offline&include_granted_scopes=true&state=state&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=code&client_id=$CLIENT_ID" +echo "" +echo -n "Code: " +read CODE + +CREDENTIALS_FILE="credentials.json" +curl -s -X POST -d 'code='$CODE'&client_id='$CLIENT_ID'&client_secret='$CLIENT_SECRET'&redirect_uri=urn:ietf:wg:oauth:2.0:oob&grant_type=authorization_code' https://www.googleapis.com/oauth2/v4/token -o $CREDENTIALS_FILE +REFRESH_TOKEN=`cat $CREDENTIALS_FILE | grep -o -e '"refresh_token": "[^"]*"' | sed -e 's/"refresh_token": "\([^"]*\)"/\1/g'` +if [ -z $REFRESH_TOKEN ]; then + echo "Failed to get refresh token. Exiting. OAuth server response:" + cat $CREDENTIALS_FILE + exit 1 +fi + +cat < $CREDENTIALS_FILE +{ + "client_id": "$CLIENT_ID", + "client_secret": "$CLIENT_SECRET", + "refresh_token": "$REFRESH_TOKEN", + "type": "authorized_user" +} +EOF +echo "" +echo "$CREDENTIALS_FILE is ready for use :-)" diff --git a/json_util.cc b/json_util.cc new file mode 100644 index 0000000..b6e102c --- /dev/null +++ b/json_util.cc @@ -0,0 +1,105 @@ +/* +Copyright 2017 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "json_util.h" +#include "scope_exit.h" + +extern "C" { +#include +} + +#include +#include + +grpc_json* GetJsonValueOrNullFromDict(grpc_json* dict_node, const char* key) { + if (dict_node->type != GRPC_JSON_OBJECT) { + return nullptr; + } + + grpc_json* child = dict_node->child; + while (child != nullptr) { + if (child->key != nullptr && strcmp(child->key, key) == 0) { + return child; + } + child = child->next; + } + return nullptr; +} + +grpc_json* GetJsonValueOrNullFromArray(grpc_json* array_node, int index) { + if (array_node->type != GRPC_JSON_ARRAY) { + return nullptr; + } + + grpc_json* child = array_node->child; + while (child != nullptr && index != 0) { + child = child->next; + index--; + } + return child; +} + +std::unique_ptr GetCustomResponseOrNull( + const std::string& device_control_request_json) { + if (device_control_request_json.length() == 0) { + return nullptr; + } + std::unique_ptr s(new char[device_control_request_json.length()]); + memcpy(s.get(), device_control_request_json.c_str(), + device_control_request_json.length()); + grpc_json* json = grpc_json_parse_string_with_len(s.get(), + device_control_request_json.length()); + if (json == nullptr) { + std::cerr << "Failed to parse json string: \"" + << device_control_request_json << "\"" << std::endl; + return nullptr; + } + ScopeExit destroy_json([json]() { + grpc_json_destroy(json); + }); + +#define RETURN_NULLPTR_IF_NULLPTR(var) if (var == nullptr) { return nullptr; } + grpc_json* inputsArray = GetJsonValueOrNullFromDict(json, "inputs"); + RETURN_NULLPTR_IF_NULLPTR(inputsArray); + grpc_json* input = GetJsonValueOrNullFromArray(inputsArray, 0); + RETURN_NULLPTR_IF_NULLPTR(input); + + grpc_json* payload = GetJsonValueOrNullFromDict(input, "payload"); + RETURN_NULLPTR_IF_NULLPTR(payload); + + grpc_json* commandsArray = GetJsonValueOrNullFromDict(payload, "commands"); + RETURN_NULLPTR_IF_NULLPTR(commandsArray); + grpc_json* command = GetJsonValueOrNullFromArray(commandsArray, 0); + RETURN_NULLPTR_IF_NULLPTR(command); + + grpc_json* executionsArray = GetJsonValueOrNullFromDict(command, "execution"); + RETURN_NULLPTR_IF_NULLPTR(executionsArray); + grpc_json* execution = GetJsonValueOrNullFromArray(executionsArray, 0); + RETURN_NULLPTR_IF_NULLPTR(execution); + + grpc_json* params = GetJsonValueOrNullFromDict(execution, "params"); + RETURN_NULLPTR_IF_NULLPTR(params); + + grpc_json* custom_response = GetJsonValueOrNullFromDict(params, "customResponse"); + RETURN_NULLPTR_IF_NULLPTR(custom_response); +#undef RETURN_NULLPTR_IF_NULLPTR + if (custom_response && custom_response->type == GRPC_JSON_STRING) { + return std::unique_ptr( + new std::string(custom_response->value)); + } + + return nullptr; +} diff --git a/json_util.h b/json_util.h new file mode 100644 index 0000000..cd80b5a --- /dev/null +++ b/json_util.h @@ -0,0 +1,48 @@ +/* +Copyright 2017 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef JSON_UTIL_H +#define JSON_UTIL_H + +#include +#include + +/* Extracts "customResponse" field from json string. E.g. + { + "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf", + "inputs": [{ + "intent": "action.devices.EXECUTE", + "payload": { + "commands": [{ + "devices": [{ + "id": "12345" + }], + "execution": [{ + "command": "action.devices.commands.Custom", + "params": { + "customResponse": "CUSTOM RESPONSE FROM 3P OEM CLOUD" + } + }] + }] + } + }] + } + Should output "CUSTOM RESPONSE FROM 3P OEM CLOUD". + */ +std::unique_ptr GetCustomResponseOrNull( + const std::string& device_control_request_json); + +#endif diff --git a/json_util_test.cc b/json_util_test.cc new file mode 100644 index 0000000..7ac81a1 --- /dev/null +++ b/json_util_test.cc @@ -0,0 +1,71 @@ +/* +Copyright 2017 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "json_util.h" + +#include + +bool check_result(const std::string& input_json, + std::unique_ptr intended_result) { + std::unique_ptr result = GetCustomResponseOrNull(input_json); + return (intended_result == nullptr && result == nullptr) + || (intended_result != nullptr && result != nullptr + && *intended_result == *result); +} + +int main() { + std::string invalid_json = ""; + std::unique_ptr intended_result(nullptr); + if (!check_result(invalid_json, std::move(intended_result))) { + std::cerr << "Test failed for invalid JSON" << std::endl; + return 1; + } + + std::string incomplete_json = "{}"; + intended_result.reset(nullptr); + if (!check_result(incomplete_json, std::move(intended_result))) { + std::cerr << "Test failed for incomplete JSON" << std::endl; + return 1; + } + + std::string valid_json = R"({ + "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf", + "inputs": [{ + "intent": "action.devices.EXECUTE", + "payload": { + "commands": [{ + "devices": [{ + "id": "12345" + }], + "execution": [{ + "command": "action.devices.commands.Custom", + "params": { + "customResponse": "CUSTOM RESPONSE FROM 3P OEM CLOUD" + } + }] + }] + } + }] + })"; + intended_result.reset( + new std::string("CUSTOM RESPONSE FROM 3P OEM CLOUD")); + if (!check_result(valid_json, std::move(intended_result))) { + std::cerr << "Test failed for valid JSON" << std::endl; + return 1; + } + + std::cerr << "Test passed" << std::endl; +} diff --git a/resources/switch_to_channel_5.raw b/resources/switch_to_channel_5.raw new file mode 100644 index 0000000..af52c05 Binary files /dev/null and b/resources/switch_to_channel_5.raw differ diff --git a/resources/weather_in_mountain_view.raw b/resources/weather_in_mountain_view.raw new file mode 100644 index 0000000..7f52ca1 Binary files /dev/null and b/resources/weather_in_mountain_view.raw differ diff --git a/run_assistant.cc b/run_assistant.cc new file mode 100644 index 0000000..81935c3 --- /dev/null +++ b/run_assistant.cc @@ -0,0 +1,298 @@ +/* +Copyright 2017 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +#define ENABLE_ALSA +#endif + +#ifdef ENABLE_ALSA +#include "audio_input_alsa.h" +#include "audio_output_alsa.h" +#endif + +#include "embedded_assistant.pb.h" +#include "embedded_assistant.grpc.pb.h" + +#include "assistant_config.h" +#include "audio_input.h" +#include "audio_input_file.h" +#include "json_util.h" +#include "service_account_util.h" + +using google::assistant::embedded::v1alpha1::EmbeddedAssistant; +using google::assistant::embedded::v1alpha1::ConverseRequest; +using google::assistant::embedded::v1alpha1::ConverseResponse; +using google::assistant::embedded::v1alpha1::AudioInConfig; +using google::assistant::embedded::v1alpha1::AudioOutConfig; +using google::assistant::embedded::v1alpha1::DeviceConfig; +using google::assistant::embedded::v1alpha1::ConverseResponse_EventType_END_OF_UTTERANCE; + +using grpc::CallCredentials; +using grpc::Channel; +using grpc::ClientReaderWriter; + +static const std::string kCredentialsTypeUserAccount = "USER_ACCOUNT"; +static const std::string kCredentialsTypeServiceAccount = "SERVICE_ACCOUNT"; +static const std::string kALSAAudioInput = "ALSA_INPUT"; + +// Creates a channel to be connected to Google. +std::shared_ptr CreateChannel(const std::string& host) { + std::ifstream file("robots.pem"); + std::stringstream buffer; + buffer << file.rdbuf(); + std::string roots_pem = buffer.str(); + + std::cout << "assistant_sdk robots_pem: " << roots_pem << std::endl; + ::grpc::SslCredentialsOptions ssl_opts = {roots_pem, "", ""}; + auto creds = ::grpc::SslCredentials(ssl_opts); + std::string server = host + ":443"; + std::cout << "assistant_sdk CreateCustomChannel(" << server << ", creds, arg)" + + << std::endl << std::endl; + ::grpc::ChannelArguments channel_args; + return CreateCustomChannel(server, creds, channel_args); +} + +void PrintUsage() { + std::cerr << "Usage: ./run_assistant " + << "--audio_input <" << kALSAAudioInput << "|] " + << "--credentials_file " + << "--credentials_type <" << kCredentialsTypeUserAccount << "|" + << kCredentialsTypeServiceAccount << "> " + << "[--api_endpoint ]" << std::endl; +} + +bool GetCommandLineFlags( + int argc, char** argv, std::string* audio_input, + std::string* credentials_file_path, std::string* credentials_type, + std::string* api_endpoint) { + const struct option long_options[] = { + {"audio_input", required_argument, nullptr, 'i'}, + {"credentials_file", required_argument, nullptr, 'f'}, + {"credentials_type", required_argument, nullptr, 't'}, + {"api_endpoint", required_argument, nullptr, 'e'}, + {nullptr, 0, nullptr, 0} + }; + *api_endpoint = ASSISTANT_ENDPOINT; + while (true) { + int option_index; + int option_char = + getopt_long(argc, argv, "i:f:t:e", long_options, &option_index); + if (option_char == -1) { + break; + } + switch (option_char) { + case 'i': + *audio_input = optarg; + break; + case 'f': + *credentials_file_path = optarg; + break; + case 't': + *credentials_type = optarg; + if (*credentials_type != kCredentialsTypeUserAccount + && *credentials_type != kCredentialsTypeServiceAccount) { + std::cerr << "Invalid credentials_type: \"" << *credentials_type + << "\". Should be \"" << kCredentialsTypeUserAccount + << "\" or \"" << kCredentialsTypeServiceAccount << "\"" + << std::endl; + return false; + } + break; + case 'e': + *api_endpoint = optarg; + break; + default: + PrintUsage(); + return false; + } + } + return true; +} + +int main(int argc, char** argv) { + std::string audio_input_source, credentials_file_path, credentials_type, + api_endpoint; + if (!GetCommandLineFlags(argc, argv, &audio_input_source, + &credentials_file_path, &credentials_type, + &api_endpoint)) { + return -1; + } + + std::unique_ptr audio_input; + if (audio_input_source == kALSAAudioInput) { +#ifdef ENABLE_ALSA + audio_input.reset(new AudioInputALSA()); +#else + std::cerr << "ALSA audio input is not supported on this platform." + << std::endl; + return -1; +#endif + } else { + std::ifstream audio_file(audio_input_source); + if (!audio_file) { + std::cerr << "Audio input file \"" << audio_input_source + << "\" does not exist." << std::endl; + return -1; + } + audio_input.reset(new AudioInputFile(audio_input_source)); + } + + // Read credentials file. + std::ifstream credentials_file(credentials_file_path); + if (!credentials_file) { + std::cerr << "Credentials file \"" << credentials_file_path + << "\" does not exist." << std::endl; + return -1; + } + std::stringstream credentials_buffer; + credentials_buffer << credentials_file.rdbuf(); + std::string credentials = credentials_buffer.str(); + std::shared_ptr call_credentials; + if (credentials_type == kCredentialsTypeServiceAccount) { + call_credentials = GetServiceAccountCredentialsOrNull(credentials); + } else { + call_credentials = grpc::GoogleRefreshTokenCredentials(credentials); + } + if (call_credentials.get() == nullptr) { + std::cerr << "Credentials file \"" << credentials_file_path + << "\" is invalid. Check step 5 in README for how to get valid " + << "credentials." << std::endl; + return -1; + } + + auto channel = CreateChannel(api_endpoint); + std::unique_ptr assistant( + EmbeddedAssistant::NewStub(channel)); + + ConverseRequest request; + auto* converse_config = request.mutable_config(); + converse_config->mutable_audio_in_config()->set_encoding( + AudioInConfig::LINEAR16); + converse_config->mutable_audio_in_config()->set_sample_rate_hertz(16000); + converse_config->mutable_audio_out_config()->set_encoding( + AudioOutConfig::LINEAR16); + converse_config->mutable_audio_out_config()->set_sample_rate_hertz(16000); + if (credentials_type == kCredentialsTypeServiceAccount) { + converse_config->mutable_converse_state()->set_is_signed_out_mode(true); + } + + // Construct Device Config. + // CUSTOMIZE: set your device_id, device_model_id and custom_context here. + DeviceConfig* config = converse_config->mutable_device_config(); + config->set_device_id("assistant-sdk-fake-device-id"); + config->set_device_model_id("my-sample-smart-tv-2017-v1"); + + auto* converse_context = + converse_config->mutable_converse_state()->mutable_context(); + converse_context->set_third_party_context("{'current_channel': 'News'}"); + + // Begin a stream. + grpc::ClientContext context; + context.set_fail_fast(false); + context.set_credentials(call_credentials); + + std::shared_ptr> + stream(std::move(assistant->Converse(&context))); + // Write config in first stream. + std::cout << "assistant_sdk wrote first request: " + << request.ShortDebugString() << std::endl; + stream->Write(request); + +#ifdef ENABLE_ALSA + AudioOutputALSA audio_output; + audio_output.Start(); +#endif + + audio_input->AddDataListener( + [stream, &request](std::shared_ptr> data) { + request.set_audio_in(&((*data)[0]), data->size()); + stream->Write(request); + }); + audio_input->AddStopListener([stream]() { + stream->WritesDone(); + }); + audio_input->Start(); + + // Read responses. + std::cout << "assistant_sdk waiting for response ... " << std::endl; + ConverseResponse response; + while (stream->Read(&response)) { // Returns false when no more to read. + std::cout << "assistant_sdk Got a response \n"; + + if ((response.has_error() || response.has_audio_out() || + response.event_type() == ConverseResponse_EventType_END_OF_UTTERANCE) + && audio_input->IsRunning()) { + // Synchronously stops audio input. + audio_input->Stop(); + } + + if (response.has_audio_out()) { + // CUSTOMIZE: play back audio_out here. + std::cout << "assistant_sdk play back audio data here." << std::endl; +#ifdef ENABLE_ALSA + std::shared_ptr> + data(new std::vector); + data->resize(response.audio_out().audio_data().length()); + memcpy(&((*data)[0]), response.audio_out().audio_data().c_str(), + response.audio_out().audio_data().length()); + audio_output.Send(data); +#endif + } + if (response.has_interim_spoken_request_text()) { + // CUSTOMIZE: render interim spoken request on screen + std::cout << "assistant_sdk response: \n" + << response.ShortDebugString() << std::endl; + } + if (response.has_device_action()) { + std::cout << "\n\nassistant_sdk device_control_json: \n" + << response.device_action().device_control_json() + << "\n\naction_type: \n" + << response.device_action().action_type() + << std::endl; + + // CUSTOMIZE: Consume device_control_request_json here, extract the + // command and custom_response and do client execution here. + } + } + +#ifdef ENABLE_ALSA + audio_output.Stop(); +#endif + + grpc::Status status = stream->Finish(); + if (!status.ok()) { + // Report the RPC failure. + std::cerr << "assistant_sdk failed, error: " << + status.error_message() << std::endl; + return -1; + } + return 0; +} diff --git a/scope_exit.h b/scope_exit.h new file mode 100644 index 0000000..37a8987 --- /dev/null +++ b/scope_exit.h @@ -0,0 +1,31 @@ +/* +Copyright 2017 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef SCOPE_EXIT_H +#define SCOPE_EXIT_H + +#include + +class ScopeExit { + public: + ScopeExit(std::function f): f_(f) {} + ~ScopeExit() { f_(); } + + private: + std::function f_; +}; + +#endif diff --git a/service_account_util.cc b/service_account_util.cc new file mode 100644 index 0000000..8daed35 --- /dev/null +++ b/service_account_util.cc @@ -0,0 +1,101 @@ +/* +Copyright 2017 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "service_account_util.h" + +#include "scope_exit.h" + +extern "C" { +#include +#include +#include +} + +#include + +static const int kAccessTokenExpirationTimeInSeconds = 3600; + +size_t write_data(void* ptr, size_t size, size_t nmemb, void* s) { + std::string* str = (std::string*) s; + size_t old_length = str->length(); + str->resize(old_length + size * nmemb); + memcpy((void*)(str->c_str() + old_length), ptr, size * nmemb); + return size * nmemb; +} + +std::shared_ptr GetServiceAccountCredentialsOrNull( + const std::string& service_account_json) { + grpc_auth_json_key json_key = grpc_auth_json_key_create_from_string( + service_account_json.c_str()); + ScopeExit destroy_json_key([&json_key]() { + grpc_auth_json_key_destruct(&json_key); + }); + if (!grpc_auth_json_key_is_valid(&json_key)) { + std::cerr << "Failed to parse service account json." << std::endl; + return nullptr; + } + + gpr_timespec token_lifetime = gpr_time_from_seconds( + kAccessTokenExpirationTimeInSeconds, GPR_TIMESPAN); + char* jwt = grpc_jwt_encode_and_sign( + &json_key, "https://www.googleapis.com/oauth2/v4/token", token_lifetime, + "https://www.googleapis.com/auth/assistant-sdk-prototype"); + if (jwt == nullptr) { + std::cerr << "Failed to sign access token request." << std::endl; + return nullptr; + } + std::string curl_data = + R"(grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=)" + std::string(jwt); + + std::string response; + CURL *hnd = curl_easy_init(); + if (hnd == nullptr) { + std::cerr << "curl_easy_init() returned null" << std::endl; + return nullptr; + } + curl_easy_setopt(hnd, CURLOPT_URL, "https://www.googleapis.com/oauth2/v4/token"); + curl_easy_setopt(hnd, CURLOPT_POSTFIELDS, curl_data.c_str()); + curl_easy_setopt(hnd, CURLOPT_WRITEFUNCTION, write_data); + curl_easy_setopt(hnd, CURLOPT_WRITEDATA, &response); + CURLcode ret = curl_easy_perform(hnd); + ScopeExit destroy_curl([hnd]() { + curl_easy_cleanup(hnd); + }); + if (ret != CURLE_OK) { + std::cerr << "curl_easy_perform() returned " << ret << std::endl; + return nullptr; + } + if (response.length() == 0) { + return nullptr; + } + + std::unique_ptr s(new char[response.length()]); + memcpy(s.get(), response.c_str(), response.length()); + grpc_json* json = grpc_json_parse_string_with_len(s.get(), response.length()); + if (json == nullptr) { + std::cerr << "Failed to parse response: \"" << response << "\"" << std::endl; + return nullptr; + } + ScopeExit destroy_json([json]() { + grpc_json_destroy(json); + }); + const char* access_token = grpc_json_get_string_property(json, "access_token"); + if (access_token == nullptr) { + std::cerr << "No access_token in response: \"" << response << "\"" << std::endl; + return nullptr; + } + return grpc::AccessTokenCredentials(std::string(access_token)); +} diff --git a/service_account_util.h b/service_account_util.h new file mode 100644 index 0000000..1270f40 --- /dev/null +++ b/service_account_util.h @@ -0,0 +1,29 @@ +/* +Copyright 2017 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef SERVICE_ACCOUNT_UTIL_H +#define SERVICE_ACCOUNT_UTIL_H + +#include + +#include + +// Generates usable credentials from service account JSON file. +// Returns nullptr if failed. +std::shared_ptr GetServiceAccountCredentialsOrNull( + const std::string& service_account_json); + +#endif \ No newline at end of file