diff --git a/ACL/include/ACL/AVSConnectionManager.h b/ACL/include/ACL/AVSConnectionManager.h index 6f217de851..5d17f42fa7 100644 --- a/ACL/include/ACL/AVSConnectionManager.h +++ b/ACL/include/ACL/AVSConnectionManager.h @@ -29,6 +29,7 @@ #include #include #include +#include #include "ACL/Transport/MessageRouterInterface.h" #include "ACL/Transport/MessageRouterObserverInterface.h" @@ -69,6 +70,8 @@ namespace acl { class AVSConnectionManager : public avsCommon::sdkInterfaces::MessageSenderInterface, public avsCommon::sdkInterfaces::AVSEndpointAssignerInterface, + /* TODO: ACSDK-421: Remove the implementation of StateSynchronizerObserverInterface */ + public avsCommon::sdkInterfaces::StateSynchronizerObserverInterface, public MessageRouterObserverInterface { public: /** @@ -77,8 +80,9 @@ class AVSConnectionManager : * @param messageRouter The entity which handles sending and receiving of AVS messages. * @param isEnabled The enablement setting. If true, then the created object will attempt to connect to AVS. * @param connectionStatusObservers An optional set of observers which will be notified when the connection status - * changes. + * changes. The observers cannot be a nullptr. * @param messageObservers An optional set of observer which will be sent messages that arrive from AVS. + * The observers cannot be a nullptr. * @return The created AVSConnectionManager object. */ static std::shared_ptr create( @@ -160,6 +164,9 @@ class AVSConnectionManager : void sendMessage(std::shared_ptr request) override; + /* TODO: ACSDK-421: Remove the implementation of StateSynchronizerObserverInterface */ + void onStateChanged(avsCommon::sdkInterfaces::StateSynchronizerObserverInterface::State newState) override; + /** * @note Set the URL endpoint for the AVS connection. Calling this function with a new value will cause the * current active connection to be closed, and a new one opened to the new endpoint. @@ -194,6 +201,10 @@ class AVSConnectionManager : /// Internal state to indicate if the Connection object is enabled for making an AVS connection. std::atomic m_isEnabled; + /* TODO: ACSDK-421: Remove the implementation of StateSynchronizerObserverInterface */ + /// Internal object that flags if @c StateSynchronizer had sent the initial event successfully. + std::atomic m_isSynchronized; + /// Set of observers to notify when the connection status changes. @c m_connectionStatusObserverMutex must be /// acquired before access. std::unordered_set> diff --git a/ACL/include/ACL/Transport/MessageRouter.h b/ACL/include/ACL/Transport/MessageRouter.h index b0506197cb..13f803d17b 100644 --- a/ACL/include/ACL/Transport/MessageRouter.h +++ b/ACL/include/ACL/Transport/MessageRouter.h @@ -69,7 +69,8 @@ class MessageRouter: public MessageRouterInterface, public TransportObserverInte ConnectionStatus getConnectionStatus() override; - void send(std::shared_ptr request) override; + // TODO: ACSDK-421: Revert this to use send(). + void sendMessage(std::shared_ptr request) override; void setAVSEndpoint(const std::string& avsEndpoint) override; diff --git a/ACL/include/ACL/Transport/MessageRouterInterface.h b/ACL/include/ACL/Transport/MessageRouterInterface.h index b31abe8a31..d925f7c9a0 100644 --- a/ACL/include/ACL/Transport/MessageRouterInterface.h +++ b/ACL/include/ACL/Transport/MessageRouterInterface.h @@ -24,6 +24,8 @@ #include "AVSCommon/Utils/Threading/Executor.h" #include "AVSCommon/AVS/MessageRequest.h" +// TODO: ACSDK-421: Revert this to implement send(). +#include "AVSCommon/SDKInterfaces/MessageSenderInterface.h" #include "ACL/Transport/MessageRouterObserverInterface.h" #include "ACL/Transport/TransportInterface.h" @@ -37,7 +39,8 @@ namespace acl { * * Implementations of this class are required to be thread-safe. */ -class MessageRouterInterface { +// TODO: ACSDK-421: Remove the inheritance from MessageSenderInterface. +class MessageRouterInterface : public avsCommon::sdkInterfaces::MessageSenderInterface { public: /// Alias to a connection status and changed reason pair. using ConnectionStatus = std::pair request) = 0; - /** * Set the URL endpoint for the AVS connection. Calling this function with a new value will cause the * current active connection to be closed, and a new one opened to the new endpoint. diff --git a/ACL/src/AVSConnectionManager.cpp b/ACL/src/AVSConnectionManager.cpp index e61af7de0c..d9ae55252e 100644 --- a/ACL/src/AVSConnectionManager.cpp +++ b/ACL/src/AVSConnectionManager.cpp @@ -43,16 +43,33 @@ AVSConnectionManager::create(std::shared_ptr messageRout std::unordered_set> connectionStatusObservers, std::unordered_set> messageObservers) { if (!avsCommon::avs::initialization::AlexaClientSDKInit::isInitialized()) { - ACSDK_ERROR(LX("createFailed").d("reason", "uninitialziedAlexaClientSdk").d("return", "nullPtr")); + ACSDK_ERROR(LX("createFailed").d("reason", "uninitialziedAlexaClientSdk").d("return", "nullptr")); return nullptr; } + if (!messageRouter) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullMessageRouter").d("return", "nullptr")); + return nullptr; + } + + for (auto observer : connectionStatusObservers) { + if (!observer) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullConnectionStatusObserver").d("return", "nullptr")); + return nullptr; + } + } + + for(auto observer : messageObservers) { + if (!observer) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullMessageObserver").d("return", "nullptr")); + return nullptr; + } + } + auto connectionManager = std::shared_ptr( new AVSConnectionManager(messageRouter, connectionStatusObservers, messageObservers)); - if (messageRouter) { - messageRouter->setObserver(connectionManager); - } + messageRouter->setObserver(connectionManager); if (isEnabled) { connectionManager->enable(); @@ -66,6 +83,7 @@ AVSConnectionManager::AVSConnectionManager( std::unordered_set> connectionStatusObservers, std::unordered_set> messageObservers) : m_isEnabled{false}, + m_isSynchronized{false}, m_connectionStatusObservers{connectionStatusObservers}, m_messageObservers{messageObservers}, m_messageRouter{messageRouter} { @@ -93,7 +111,13 @@ void AVSConnectionManager::reconnect() { } void AVSConnectionManager::sendMessage(std::shared_ptr request) { - m_messageRouter->send(request); + // TODO: ACSDK-421: Implement synchronized state check at a lower level. + if (m_isSynchronized) { + m_messageRouter->sendMessage(request); + } else { + ACSDK_DEBUG(LX("sendMessageNotSuccessful").d("reason", "notSynchronized")); + request->onSendCompleted(avsCommon::avs::MessageRequest::Status::NOT_SYNCHRONIZED); + } } bool AVSConnectionManager::isConnected() const { @@ -167,6 +191,10 @@ void AVSConnectionManager::onConnectionStatusChanged( } } +void AVSConnectionManager::onStateChanged(StateSynchronizerObserverInterface::State newState) { + m_isSynchronized = (StateSynchronizerObserverInterface::State::SYNCHRONIZED == newState); +} + void AVSConnectionManager::receive(const std::string & contextId, const std::string & message) { std::unique_lock lock{m_messageOberverMutex}; std::unordered_set> observers{m_messageObservers}; diff --git a/ACL/src/Transport/HTTP2Stream.cpp b/ACL/src/Transport/HTTP2Stream.cpp index e20a79942a..1205447ee7 100644 --- a/ACL/src/Transport/HTTP2Stream.cpp +++ b/ACL/src/Transport/HTTP2Stream.cpp @@ -28,6 +28,7 @@ using namespace alexaClientSDK::avsCommon::utils; using namespace avsCommon::avs; using namespace avsCommon::avs::attachment; + /// String to identify log entries originating from this file. static const std::string TAG("HTTP2Stream"); @@ -209,20 +210,24 @@ bool HTTP2Stream::initPost(const std::string& url, const std::string& authToken, } if (!m_transfer.setPostContent(METADATA_FIELD_NAME, requestPayload)) { + ACSDK_ERROR(LX("initPostFailed").d("reason", "setPostContentFailed")); return false; } if (!m_transfer.setReadCallback(HTTP2Stream::readCallback, this)) { + ACSDK_ERROR(LX("initPostFailed").d("reason", "setReadCallbackFailed")); return false; } if (request->getAttachmentReader()) { if (!m_transfer.setPostStream(ATTACHMENT_FIELD_NAME, this)) { + ACSDK_ERROR(LX("initPostFailed").d("reason", "setPostStreamFailed")); return false; } } if (!m_transfer.setTransferType(CurlEasyHandleWrapper::TransferType::kPOST)) { + ACSDK_ERROR(LX("initPostFailed").d("reason", "setTransferTypeFailed")); return false; } @@ -262,6 +267,10 @@ size_t HTTP2Stream::writeCallback(char *data, size_t size, size_t nmemb, void *u } size_t HTTP2Stream::headerCallback(char *data, size_t size, size_t nmemb, void *user) { + if (!user) { + ACSDK_ERROR(LX("headerCallbackFailed").d("reason","nullUser")); + return 0; + } size_t headerLength = size * nmemb; std::string header(data, headerLength); #ifdef DEBUG @@ -285,9 +294,14 @@ size_t HTTP2Stream::headerCallback(char *data, size_t size, size_t nmemb, void * } size_t HTTP2Stream::readCallback(char *data, size_t size, size_t nmemb, void *userData) { + if (!userData) { + ACSDK_ERROR(LX("readCallbackFailed").d("reason","nullUserData")); + return 0; + } + HTTP2Stream *stream = static_cast(userData); - stream->m_timeOfLastTransfer = getNow(); + stream->m_timeOfLastTransfer = getNow(); auto attachmentReader = stream->m_currentRequest->getAttachmentReader(); // This is ok - it means there's no attachment to send. Return 0 so libcurl can complete the stream to AVS. @@ -301,7 +315,6 @@ size_t HTTP2Stream::readCallback(char *data, size_t size, size_t nmemb, void *us auto bytesRead = attachmentReader->read(data, maxBytesToRead, &readStatus); switch (readStatus) { - // The good cases. case AttachmentReader::ReadStatus::OK: case AttachmentReader::ReadStatus::OK_WOULDBLOCK: @@ -318,7 +331,6 @@ size_t HTTP2Stream::readCallback(char *data, size_t size, size_t nmemb, void *us case AttachmentReader::ReadStatus::ERROR_INTERNAL: return CURL_READFUNC_ABORT; } - // The attachment has no more data right now, but is still readable. if (0 == bytesRead) { stream->setPaused(true); diff --git a/ACL/src/Transport/HTTP2Transport.cpp b/ACL/src/Transport/HTTP2Transport.cpp index 84265eba3d..6983b282c3 100644 --- a/ACL/src/Transport/HTTP2Transport.cpp +++ b/ACL/src/Transport/HTTP2Transport.cpp @@ -344,11 +344,9 @@ void HTTP2Transport::networkLoop() { * While the connection is alive we should have at least 1 transfer active (the downchannel). */ int numTransfersLeft = 1; + int timeouts = 0; while (numTransfersLeft && !isStopping()) { - int numTransfersUpdated = 0; - int timeouts = 0; - CURLMcode ret = curl_multi_perform(m_multi->handle, &numTransfersLeft); if (CURLM_CALL_MULTI_PERFORM == ret) { continue; @@ -389,6 +387,7 @@ void HTTP2Transport::networkLoop() { //TODO: ACSDK-69 replace timeout with signal fd //TODO: ACSDK-281 - investigate the timeout values and performance consequences for curl_multi_wait. + int numTransfersUpdated = 0; ret = curl_multi_wait(m_multi->handle, NULL, 0, multiWaitTimeoutMs, &numTransfersUpdated); if (ret != CURLM_OK) { ACSDK_ERROR(LX("networkLoopStopping") @@ -487,7 +486,6 @@ void HTTP2Transport::networkLoop() { bool HTTP2Transport::establishConnection() { // Set numTransferLeft to 1 because the downchannel stream has been added already. int numTransfersLeft = 1; - int numTransfersUpdated = 0; /* * Calls curl_multi_perform until downchannel stream receives an HTTP2 response code. If the downchannel stream @@ -526,6 +524,7 @@ bool HTTP2Transport::establishConnection() { setIsStopping(ConnectionStatusObserverInterface::ChangedReason::INTERNAL_ERROR); } // wait for activity on the downchannel stream, kinda like poll() + int numTransfersUpdated = 0; ret = curl_multi_wait(m_multi->handle, NULL, 0 , WAIT_FOR_ACTIVITY_TIMEOUT_MS, &numTransfersUpdated); if (ret != CURLM_OK) { ACSDK_ERROR(LX("establishConnectionFailed") @@ -648,6 +647,8 @@ void HTTP2Transport::processNextOutgoingMessage() { } bool HTTP2Transport::sendPing() { + ACSDK_DEBUG(LX("sendPing").d("pingStream", m_pingStream.get())); + if (m_pingStream) { return true; } @@ -687,6 +688,7 @@ bool HTTP2Transport::sendPing() { } void HTTP2Transport::handlePingResponse() { + ACSDK_DEBUG(LX("handlePingResponse")); if (HTTP2Stream::HTTPResponseCodes::SUCCESS_NO_CONTENT != m_pingStream->getResponseCode()) { ACSDK_ERROR(LX("pingFailed") .d("responseCode", m_pingStream->getResponseCode())); diff --git a/ACL/src/Transport/MessageRouter.cpp b/ACL/src/Transport/MessageRouter.cpp index 99d8212e46..3b707f116d 100644 --- a/ACL/src/Transport/MessageRouter.cpp +++ b/ACL/src/Transport/MessageRouter.cpp @@ -80,7 +80,8 @@ void MessageRouter::disable() { disconnectAllTransportsLocked(lock, ConnectionStatusObserverInterface::ChangedReason::ACL_CLIENT_REQUEST); } -void MessageRouter::send(std::shared_ptr request) { +// TODO: ACSDK-421: Revert this to use send(). +void MessageRouter::sendMessage(std::shared_ptr request) { if (!request) { ACSDK_ERROR(LX("sendFailed").d("reason", "nullRequest")); return; diff --git a/ACL/test/AVSConnectionManagerTest.cpp b/ACL/test/AVSConnectionManagerTest.cpp index 11e3e0a82c..a21625e6c6 100644 --- a/ACL/test/AVSConnectionManagerTest.cpp +++ b/ACL/test/AVSConnectionManagerTest.cpp @@ -15,9 +15,11 @@ * permissions and limitations under the License. */ +/// @file AVSConnectionManagerTest.cpp + #include #include - +#include #include "ACL/AVSConnectionManager.h" namespace alexaClientSDK { @@ -25,48 +27,123 @@ namespace acl { namespace test { using namespace ::testing; +using namespace alexaClientSDK::avsCommon::avs::initialization; using namespace alexaClientSDK::avsCommon::sdkInterfaces; + +/// This class allows us to test MessageObserver interaction +class MockMessageObserver : public MessageObserverInterface { +public: + MockMessageObserver() { } + MOCK_METHOD2(receive, void(const std::string & contextId, const std::string & message)); +}; + +/// This class allows us to test ConnectionStatusObserver interaction +class MockConnectionStatusObserver : public ConnectionStatusObserverInterface { +public: + MockConnectionStatusObserver() { } + MOCK_METHOD2(onConnectionStatusChanged, void(ConnectionStatusObserverInterface::Status status, + ConnectionStatusObserverInterface::ChangedReason reason)); +}; + /** * This class allows us to test MessageRouter interaction. */ class MockMessageRouter : public MessageRouterInterface { public: - MockMessageRouter() { } MOCK_METHOD0(enable, void()); MOCK_METHOD0(disable, void()); MOCK_METHOD0(getConnectionStatus, MessageRouterInterface::ConnectionStatus()); - MOCK_METHOD1(send, void(std::shared_ptr request)); + // TODO: ACSDK-421: Revert this to use send(). + MOCK_METHOD1(sendMessage, void(std::shared_ptr request)); MOCK_METHOD1(setAVSEndpoint, void(const std::string& avsEndpoint)); MOCK_METHOD1(setObserver, void(std::shared_ptr observer)); - }; +/// Test harness for @c AVSConnectionManager class class AVSConnectionManagerTest : public ::testing::Test { public: - AVSConnectionManagerTest() { - m_messageRouter = std::make_shared(); - m_avsConnectionManager = AVSConnectionManager::create( - m_messageRouter, - true, - std::unordered_set>(), - std::unordered_set>()); - } + void SetUp() override; + void TearDown() override; -protected: std::shared_ptr m_avsConnectionManager; std::shared_ptr m_messageRouter; + std::shared_ptr m_observer; + std::shared_ptr m_messageObserver; }; +void AVSConnectionManagerTest::SetUp() { + AlexaClientSDKInit::initialize(std::vector()); + m_messageRouter = std::make_shared(); + m_observer = std::make_shared(); + m_messageObserver = std::make_shared(); + m_avsConnectionManager = AVSConnectionManager::create( + m_messageRouter, + true, + std::unordered_set>(), + std::unordered_set>()); + m_avsConnectionManager->onStateChanged(StateSynchronizerObserverInterface::State::SYNCHRONIZED); +} + +void AVSConnectionManagerTest::TearDown() { + AlexaClientSDKInit::uninitialize(); +} + +/** + * Test @c create with valid messageRouter, ConnectionStatusObserver, MessageObservers + */ +TEST_F(AVSConnectionManagerTest, createTest) { + EXPECT_CALL(*m_messageRouter, setObserver(_)).Times(1); + EXPECT_CALL(*m_messageRouter, enable()).Times(1); + ASSERT_NE(nullptr, m_avsConnectionManager->create(m_messageRouter, true, {m_observer}, {m_messageObserver})); +} + +/** + * Test @c create with different combinations of messageRouter, ConnectionStatusObserver, MessageObservers + */ +TEST_F(AVSConnectionManagerTest, createWithNullMessageRouterAndObservers) { + ASSERT_EQ(nullptr, m_avsConnectionManager->create(nullptr, true, {m_observer}, {m_messageObserver})); + ASSERT_EQ(nullptr, m_avsConnectionManager->create(m_messageRouter, true, {nullptr}, {m_messageObserver})); + ASSERT_EQ(nullptr, m_avsConnectionManager->create(m_messageRouter, true, {m_observer}, {nullptr})); + std::shared_ptr validConnectionStatusObserver; + validConnectionStatusObserver = std::make_shared(); + ASSERT_EQ(nullptr, m_avsConnectionManager->create( + m_messageRouter, true, {m_observer, nullptr, validConnectionStatusObserver}, {m_messageObserver})); + std::shared_ptr validMessageObserver; + validMessageObserver = std::make_shared(); + ASSERT_EQ(nullptr, m_avsConnectionManager->create( + m_messageRouter, true, {m_observer}, {m_messageObserver, nullptr, validMessageObserver})); + ASSERT_EQ(nullptr, m_avsConnectionManager->create(m_messageRouter, true, {nullptr}, {nullptr})); + // create should pass with empty set of ConnectionStatusObservers + ASSERT_NE(nullptr, m_avsConnectionManager->create(m_messageRouter, true, + std::unordered_set>(), {validMessageObserver})); + // create should pass with empty set of MessageObservers + ASSERT_NE(nullptr, m_avsConnectionManager->create(m_messageRouter, true, + {validConnectionStatusObserver}, std::unordered_set>())); + // create should pass with valid messageRouter, ConnectionStatusObservers and MessageObservers + ASSERT_NE(nullptr, m_avsConnectionManager->create( + m_messageRouter, true, {validConnectionStatusObserver}, {validMessageObserver})); +} + /** * Test addConnectionStatusObserver with a @c nullptr observer, expecting no errors. */ TEST_F(AVSConnectionManagerTest, addConnectionStatusObserverNull) { + EXPECT_CALL(*m_messageRouter, getConnectionStatus()).Times(0); m_avsConnectionManager->addConnectionStatusObserver(nullptr); } +/** + * Test with addConnectionStatusObserver with MockConnectionStatusObserver. + */ +TEST_F(AVSConnectionManagerTest, addConnectionStatusObserverValid) { + EXPECT_CALL(*m_messageRouter, getConnectionStatus()).Times(1); + EXPECT_CALL(*m_observer, onConnectionStatusChanged(_, _)).Times(1); + m_avsConnectionManager->addConnectionStatusObserver(m_observer); +} + /** * Test removeConnectionStatusObserver with a @c nullptr observer, expecting no errors. */ @@ -88,6 +165,44 @@ TEST_F(AVSConnectionManagerTest, removeMessageObserverNull) { m_avsConnectionManager->removeMessageObserver(nullptr); } +/** + * Test enable and disable function of AVSConnectionManager + */ +TEST_F(AVSConnectionManagerTest, enableAndDisableFunction) { + EXPECT_CALL(*m_messageRouter, enable()).Times(1); + m_avsConnectionManager->enable(); + ASSERT_TRUE(m_avsConnectionManager->isEnabled()); + EXPECT_CALL(*m_messageRouter, disable()).Times(1); + m_avsConnectionManager->disable(); + ASSERT_FALSE(m_avsConnectionManager->isEnabled()); +} + +/** + * Tests sendMessage with a @c nullptr request, expecting no errors. + */ +TEST_F(AVSConnectionManagerTest, sendMessageRequestTest) { + // TODO: ACSDK-421: Revert this to use send(). + EXPECT_CALL(*m_messageRouter, sendMessage(_)).Times(1); + m_avsConnectionManager->sendMessage(nullptr); + // TODO: ACSDK-421: Revert this to use send(). + EXPECT_CALL(*m_messageRouter, sendMessage(_)).Times(1); + std::shared_ptr messageRequest; + messageRequest = std::make_shared("Test message", nullptr); + m_avsConnectionManager->sendMessage(messageRequest); +} + +/** + * Test setAVSEndpoint and expect a call to messageRouter's setAVSEndpoint. + */ +TEST_F(AVSConnectionManagerTest, setAVSEndpointTest) { + EXPECT_CALL(*m_messageRouter, setAVSEndpoint(_)).Times(1); + m_avsConnectionManager->setAVSEndpoint("AVSEndpoint"); +} } // namespace test } // namespace acl } // namespace alexaClientSDK + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/ACL/test/Transport/HTTP2StreamPoolTest.cpp b/ACL/test/Transport/HTTP2StreamPoolTest.cpp index 7f4878df30..a089e73f00 100644 --- a/ACL/test/Transport/HTTP2StreamPoolTest.cpp +++ b/ACL/test/Transport/HTTP2StreamPoolTest.cpp @@ -96,7 +96,6 @@ TEST_F(HTTP2StreamPoolTest, initGetFails) { ASSERT_EQ(stream, nullptr); stream = m_testableStreamPool->createGetStream(TEST_LIBCURL_URL, LIBCURL_TEST_AUTH_STRING, nullptr); ASSERT_EQ(stream, nullptr); - stream = m_testableStreamPool->createGetStream(TEST_LIBCURL_URL, LIBCURL_TEST_AUTH_STRING, &m_testableConsumer); } /** diff --git a/ACL/test/Transport/HTTP2StreamTest.cpp b/ACL/test/Transport/HTTP2StreamTest.cpp index a48975a6c4..aa84b3d58c 100644 --- a/ACL/test/Transport/HTTP2StreamTest.cpp +++ b/ACL/test/Transport/HTTP2StreamTest.cpp @@ -24,7 +24,9 @@ #include #include - +#include +#include +#include #include "TestableConsumer.h" #include "MockMessageRequest.h" #include "Common/Common.h" @@ -33,48 +35,96 @@ namespace alexaClientSDK { namespace acl { namespace test { +using namespace ::testing; +using namespace avsCommon::avs; +using namespace avsCommon::avs::attachment; +using namespace avsCommon::utils::sds; +using namespace alexaClientSDK::avsCommon::avs::initialization; + /// A test url with which to initialize our test stream object. static const std::string LIBCURL_TEST_URL = "http://example.com"; /// A test auth string with which to initialize our test stream object. static const std::string LIBCURL_TEST_AUTH_STRING = "test_auth_string"; -/// The lenth of the string we will test for the exception message. +/// The length of the string we will test for the exception message. static const int TEST_EXCEPTION_STRING_LENGTH = 200; /// The number of iterations the multi-write test will perform. static const int TEST_EXCEPTION_PARTITIONS = 7; - +/// Number of bytes per word in the SDS circular buffer. +static const size_t SDS_WORDSIZE = 1; +/// Maximum number of readers to support in the SDS circular buffer. +static const size_t SDS_MAXREADERS = 1; +/// Number of words to hold in the SDS circular buffer. +static const size_t SDS_WORDS = 300; +/// Number of strings to read/write for the test +static const size_t NUMBER_OF_STRINGS = 1; /** * Our GTest class. */ class HTTP2StreamTest : public ::testing::Test { public: - /** - * Construct the objects we will use across tests. - */ - void SetUp() override { - // We won't be using the network explictly, but a HTTP2Stream object requires curl to be initialized - // to work correctly. - curl_global_init(CURL_GLOBAL_ALL); + void SetUp() override; - m_mockMessageRequest = std::make_shared(); - m_testableStream = std::make_shared(&m_testableConsumer, nullptr); - m_testableStream->initPost(LIBCURL_TEST_URL, LIBCURL_TEST_AUTH_STRING, m_mockMessageRequest); - } - - /** - * Clean up. - */ - void TearDown() override { - curl_global_cleanup(); - } + void TearDown() override; + /// A message request to initiate @c m_readTestableStream with + std::shared_ptr m_MessageRequest; /// A mock message request object. std::shared_ptr m_mockMessageRequest; /// An object that is required for constructing a stream. TestableConsumer m_testableConsumer; /// The actual stream we will be testing. std::shared_ptr m_testableStream; + /// The stream to test @c readCallback function + std::shared_ptr m_readTestableStream; + /// The attachment manager for the stream + std::shared_ptr m_attachmentManager; + /// A Writer to write data to SDS buffer. + std::unique_ptr m_writer; + /// The attachment reader for message request of @c m_readTestableStream + std::unique_ptr m_attachmentReader; + /// A char pointer to data buffer to read or write from callbacks + char* m_dataBegin; + /// A string to which @c m_dataBegin is pointing to + std::string m_testString; }; +void HTTP2StreamTest::SetUp() { + AlexaClientSDKInit::initialize(std::vector()); + + m_testString = createRandomAlphabetString(TEST_EXCEPTION_STRING_LENGTH); + m_dataBegin = const_cast(m_testString.c_str()); + + /// Create a SDS buffer and using a @c Writer, write a string into the buffer + size_t bufferSize = InProcessSDS::calculateBufferSize(SDS_WORDS, SDS_WORDSIZE, SDS_MAXREADERS); + auto buffer = std::make_shared(bufferSize); + std::shared_ptr stream = InProcessSDS::create(buffer, SDS_WORDSIZE, SDS_MAXREADERS); + ASSERT_NE(stream, nullptr); + + m_writer = stream->createWriter(InProcessSDS::Writer::Policy::NONBLOCKABLE); + ASSERT_NE(m_writer, nullptr); + ASSERT_EQ(TEST_EXCEPTION_STRING_LENGTH, m_writer->write(m_dataBegin, TEST_EXCEPTION_STRING_LENGTH)); + + /// Create an attachment Reader for @c m_MessageRequest + m_attachmentReader = InProcessAttachmentReader::create(AttachmentReader::Policy::NON_BLOCKING, stream); + m_MessageRequest = std::make_shared("", std::move(m_attachmentReader)); + ASSERT_NE(m_MessageRequest, nullptr); + + m_mockMessageRequest = std::make_shared(); + ASSERT_NE(m_mockMessageRequest, nullptr); + m_attachmentManager = std::make_shared(AttachmentManager::AttachmentType::IN_PROCESS); + + m_testableStream = std::make_shared(&m_testableConsumer, m_attachmentManager); + ASSERT_NE(m_testableStream, nullptr); + ASSERT_TRUE(m_testableStream->initPost(LIBCURL_TEST_URL, LIBCURL_TEST_AUTH_STRING, m_mockMessageRequest)); + + m_readTestableStream = std::make_shared(&m_testableConsumer, m_attachmentManager); + ASSERT_NE(m_readTestableStream, nullptr); + ASSERT_TRUE(m_readTestableStream->initPost(LIBCURL_TEST_URL, LIBCURL_TEST_AUTH_STRING, m_MessageRequest)); + } + +void HTTP2StreamTest::TearDown() { + AlexaClientSDKInit::uninitialize(); + } /** * Let's simulate that AVSConnectionManager->send() has been invoked, and the messageRequest object is * waiting to be notified on the response from AVS. We will invoke the stream writeCallbacks directly to @@ -82,13 +132,10 @@ class HTTP2StreamTest : public ::testing::Test { * the request object. */ TEST_F(HTTP2StreamTest, testExceptionReceivedSingleWrite) { - auto testString = createRandomAlphabetString(TEST_EXCEPTION_STRING_LENGTH); - char* dataBegin = reinterpret_cast(&testString[0]); - - HTTP2Stream::writeCallback(dataBegin, TEST_EXCEPTION_STRING_LENGTH, 1, m_testableStream.get()); - - EXPECT_CALL(*m_mockMessageRequest, onExceptionReceived(testString)).Times(1); + HTTP2Stream::writeCallback(m_dataBegin, TEST_EXCEPTION_STRING_LENGTH, NUMBER_OF_STRINGS, m_testableStream.get()); + EXPECT_CALL(*m_mockMessageRequest, onExceptionReceived(_)).Times(1); + EXPECT_CALL(*m_mockMessageRequest, onSendCompleted(_)).Times(1); // This simulates stream cleanup, which flushes out the parsed exception message. m_testableStream->notifyRequestObserver(); } @@ -98,33 +145,51 @@ TEST_F(HTTP2StreamTest, testExceptionReceivedSingleWrite) { * long exception message). */ TEST_F(HTTP2StreamTest, testExceptionReceivedMultiWrite) { - auto testString = createRandomAlphabetString(TEST_EXCEPTION_STRING_LENGTH); - char* dataBegin = reinterpret_cast(&testString[0]); - int writeQuantum = TEST_EXCEPTION_STRING_LENGTH; if (TEST_EXCEPTION_PARTITIONS > 0) { writeQuantum = TEST_EXCEPTION_STRING_LENGTH / TEST_EXCEPTION_PARTITIONS; } - int numberBytesWritten = 0; - char* currBuffPosition = dataBegin; + char* currBuffPosition = m_dataBegin; while (numberBytesWritten < TEST_EXCEPTION_STRING_LENGTH) { - int bytesRemaining = testString.length() - numberBytesWritten; + int bytesRemaining = TEST_EXCEPTION_STRING_LENGTH - numberBytesWritten; int bytesToWrite = bytesRemaining < writeQuantum ? bytesRemaining : writeQuantum; - HTTP2Stream::writeCallback(currBuffPosition, bytesToWrite, 1, m_testableStream.get()); + HTTP2Stream::writeCallback(currBuffPosition, bytesToWrite, NUMBER_OF_STRINGS, m_testableStream.get()); currBuffPosition += bytesToWrite; numberBytesWritten += bytesToWrite; } - EXPECT_CALL(*m_mockMessageRequest, onExceptionReceived(testString)).Times(1); + EXPECT_CALL(*m_mockMessageRequest, onExceptionReceived(_)).Times(1); + EXPECT_CALL(*m_mockMessageRequest, onSendCompleted(_)).Times(1); // This simulates stream cleanup, which flushes out the parsed exception message. m_testableStream->notifyRequestObserver(); } +TEST_F(HTTP2StreamTest, testHeaderCallback) { + // Check if the length returned is as expected + int headerLength = TEST_EXCEPTION_STRING_LENGTH * NUMBER_OF_STRINGS; + int returnHeaderLength = HTTP2Stream::headerCallback( + m_dataBegin, TEST_EXCEPTION_STRING_LENGTH, NUMBER_OF_STRINGS, m_testableStream.get()); + ASSERT_EQ(headerLength, returnHeaderLength); + // Call the function with NULL HTTP2Stream and check if it fails + returnHeaderLength = HTTP2Stream::headerCallback( + m_dataBegin, TEST_EXCEPTION_STRING_LENGTH, NUMBER_OF_STRINGS, nullptr); + ASSERT_EQ(0, returnHeaderLength); +} + +TEST_F(HTTP2StreamTest, testReadCallBack) { + //Check if the bytesRead are equal to length of data written in SDS buffer + int bytesRead = HTTP2Stream::readCallback( + m_dataBegin, TEST_EXCEPTION_STRING_LENGTH, NUMBER_OF_STRINGS, m_readTestableStream.get()); + ASSERT_EQ(TEST_EXCEPTION_STRING_LENGTH, bytesRead); + // Call the function with NULL HTTP2Stream and check if it fails + bytesRead = HTTP2Stream::readCallback(m_dataBegin, TEST_EXCEPTION_STRING_LENGTH, NUMBER_OF_STRINGS, nullptr); + ASSERT_EQ(0, bytesRead); +} } // namespace test } // namespace acl } // namespace alexaClientSDK diff --git a/ACL/test/Transport/MessageRouterTest.cpp b/ACL/test/Transport/MessageRouterTest.cpp index c8e61097b0..90b4164c21 100644 --- a/ACL/test/Transport/MessageRouterTest.cpp +++ b/ACL/test/Transport/MessageRouterTest.cpp @@ -104,7 +104,8 @@ TEST_F(MessageRouterTest, sendIsSuccessfulWhenConnected) { // Expect to have the message sent to the transport EXPECT_CALL(*m_mockTransport, send(messageRequest)).Times(1); - m_router.send(messageRequest); + // TODO: ACSDK-421: Revert this to use send(). + m_router.sendMessage(messageRequest); // Since we connected we will be disconnected when the router is destroyed EXPECT_CALL(*m_mockTransport, disconnect()).Times(AnyNumber()); @@ -116,7 +117,8 @@ TEST_F(MessageRouterTest, sendFailsWhenDisconnected) { // Expect to have the message sent to the transport EXPECT_CALL(*m_mockTransport, send(messageRequest)).Times(0); - m_router.send(messageRequest); + // TODO: ACSDK-421: Revert this to use send(). + m_router.sendMessage(messageRequest); } TEST_F(MessageRouterTest, sendFailsWhenPending) { @@ -129,7 +131,8 @@ TEST_F(MessageRouterTest, sendFailsWhenPending) { // Expect to have the message sent to the transport. EXPECT_CALL(*m_mockTransport, send(messageRequest)).Times(1); - m_router.send(messageRequest); + // TODO: ACSDK-421: Revert this to use send(). + m_router.sendMessage(messageRequest); waitOnMessageRouter(SHORT_TIMEOUT_MS); } @@ -144,7 +147,8 @@ TEST_F(MessageRouterTest, sendMessageDoesNotSendAfterDisconnected) { // Expect to have the message sent to the transport EXPECT_CALL(*m_mockTransport, send(messageRequest)).Times(0); - m_router.send(messageRequest); + // TODO: ACSDK-421: Revert this to use send(). + m_router.sendMessage(messageRequest); } TEST_F(MessageRouterTest, disconnectDisconnectsConnectedTransports) { @@ -201,11 +205,34 @@ TEST_F(MessageRouterTest, serverSideDisconnectCreatesANewTransport) { EXPECT_CALL(*newTransport.get(), send(messageRequest)).Times(1); - m_router.send(messageRequest); + // TODO: ACSDK-421: Revert this to use send(). + m_router.sendMessage(messageRequest); waitOnMessageRouter(SHORT_TIMEOUT_MS); } +/** + * This tests the calling of private method @c receive() for MessageRouterObserver from MessageRouter + */ +TEST_F(MessageRouterTest, onReceiveTest) { + m_mockMessageRouterObserver->reset(); + m_router.consumeMessage(CONTEXT_ID, MESSAGE); + waitOnMessageRouter(SHORT_TIMEOUT_MS); + ASSERT_TRUE(m_mockMessageRouterObserver->wasNotifiedOfReceive()); + ASSERT_EQ(CONTEXT_ID, m_mockMessageRouterObserver->getAttachmentContextId()); + ASSERT_EQ(MESSAGE, m_mockMessageRouterObserver->getLatestMessage()); +} + +/** + * This tests the calling of private method @c onConnectionStatusChanged() + * for MessageRouterObserver from MessageRouter + */ +TEST_F(MessageRouterTest, onConnectionStatusChangedTest) { + m_mockMessageRouterObserver->reset(); + setupStateToConnected(); + waitOnMessageRouter(SHORT_TIMEOUT_MS); + ASSERT_TRUE(m_mockMessageRouterObserver->wasNotifiedOfStatusChange()); +} } // namespace test } // namespace acl } // namespace alexaClientSDK diff --git a/ACL/test/Transport/MessageRouterTest.h b/ACL/test/Transport/MessageRouterTest.h index 16bbd51040..5fd70356b3 100644 --- a/ACL/test/Transport/MessageRouterTest.h +++ b/ACL/test/Transport/MessageRouterTest.h @@ -128,6 +128,7 @@ class MessageRouterTest : public ::testing::Test { static const std::string MESSAGE; static const int MESSAGE_LENGTH; static const std::chrono::milliseconds SHORT_TIMEOUT_MS; + static const std::string CONTEXT_ID; std::shared_ptr m_mockMessageRouterObserver; std::shared_ptr m_mockAuthDelegate; @@ -139,6 +140,7 @@ class MessageRouterTest : public ::testing::Test { const std::string MessageRouterTest::MESSAGE = "123456789"; const int MessageRouterTest::MESSAGE_LENGTH = 10; const std::chrono::milliseconds MessageRouterTest::SHORT_TIMEOUT_MS = std::chrono::milliseconds(1000); +const std::string MessageRouterTest::CONTEXT_ID = "contextIdString"; } // namespace test } // namespace acl diff --git a/ACL/test/Transport/MockMessageRequest.h b/ACL/test/Transport/MockMessageRequest.h index 0353f2eb68..ad53af8b7c 100644 --- a/ACL/test/Transport/MockMessageRequest.h +++ b/ACL/test/Transport/MockMessageRequest.h @@ -38,6 +38,8 @@ class MockMessageRequest : public avsCommon::avs::MessageRequest { */ MockMessageRequest() : avsCommon::avs::MessageRequest{"", nullptr} { } MOCK_METHOD1(onExceptionReceived, void(const std::string & exceptionMessage)); + MOCK_METHOD1(onSendCompleted, void(Status status)); + }; } // namespace test diff --git a/ACL/test/Transport/MockMessageRouterObserver.h b/ACL/test/Transport/MockMessageRouterObserver.h index 516e1ed7e0..127531faa9 100644 --- a/ACL/test/Transport/MockMessageRouterObserver.h +++ b/ACL/test/Transport/MockMessageRouterObserver.h @@ -56,6 +56,10 @@ class MockMessageRouterObserver: public MessageRouterObserverInterface { return m_message; } + std::string getAttachmentContextId() { + return m_attachmentContextId; + } + private: virtual void onConnectionStatusChanged( const avsCommon::sdkInterfaces::ConnectionStatusObserverInterface::Status status, diff --git a/ADSL/include/ADSL/DirectiveProcessor.h b/ADSL/include/ADSL/DirectiveProcessor.h index 094906befe..457357091a 100644 --- a/ADSL/include/ADSL/DirectiveProcessor.h +++ b/ADSL/include/ADSL/DirectiveProcessor.h @@ -39,14 +39,16 @@ namespace adsl { * @par * @c DirectiveProcessor receives directives via its @c onDirective() method. The @c dialogRequestId property of * incoming directives is checked against the current @c dialogRequestId (which is set by @c setDialogRequestId()). - * If the values do not match, the @c AVSDirective is dropped, and @c onDirective() returns @c true to indicate that - * the @c AVSDirective has been consumed (in this case, because it is not longer relevant). + * If the @c AVSDirective's value is not empty and does not match, the @c AVSDirective is dropped, and + * @c onDirective() returns @c true to indicate that the @c AVSDirective has been consumed (in this case, because + * it is not longer relevant). * @par * After passing this hurdle, the @c AVSDirective is forwarded to the @c preHandleDirective() method of whichever * @c DirectiveHandler is registered to handle the @c AVSDirective. If no @c DirectiveHandler is registered, the - * incoming directive is rejected and any directives already queued for handling by the @c DirectiveProcessor are - * canceled (because an entire dialog is canceled when the handling of any of its directives fails), and - * @c onDirective() returns @c false to indicate that the unhandled @c AVDirective was rejected. + * incoming directive is rejected and any directives with the same dialogRequestId that are already queued for + * handling by the @c DirectiveProcessor are canceled (because an entire dialog is canceled when the handling of + * any of its directives fails), and @c onDirective() returns @c false to indicate that the unhandled @c AVDirective + * was rejected. * @par * Once an @c AVSDirective has been successfully forwarded for preHandling, it is enqueued awaiting its turn to be * handled. Handling is accomplished by forwarding the @c AVSDirective to the @c handleDirective() method of @@ -131,64 +133,32 @@ class DirectiveProcessor { /// Handle of the @c DirectiveProcessor to forward notifications to. ProcessorHandle m_processorHandle; - /// The @c messageId of the @c AVSDirective whose handling result will be specified by this instance. - std::string m_messageId; + /// The @c AVSDirective whose handling result will be specified by this instance. + std::shared_ptr m_directive; }; /** * Receive notification that the handling of an @c AVSDirective has completed. * - * @param messageId The @c messageId of the @c AVSDirective whose handling has completed. + * @param directive The @c AVSDirective whose handling has completed. */ - void onHandlingCompleted(const std::string& messageId); + void onHandlingCompleted(std::shared_ptr directive); /** * Receive notification that the handling of an @c AVSDirective has failed. * - * @param messageId The @c messageId of the @c AVSDirective whose handling has failed. + * @param directive The @c AVSDirective whose handling has failed. * @param description A description (suitable for logging diagnostics) that indicates the nature of the failure. */ - void onHandlingFailed(const std::string& messageId, const std::string& description); + void onHandlingFailed(std::shared_ptr directive, const std::string& description); /** - * Remove an @c AVSDirective from @c m_handlingQueue. - * @note This method must only be called by threads that have acquired @c m_contextMutex. + * Remove an @c AVSDirective from processing. + * @note This method must only be called by threads that have acquired @c m_mutex. * - * @param messagId The @c messageId of the @c AVSDirective to remove. - * @return Whether the @c AVSDirective was actually removed. + * @param directive The @c AVSDirective to remove from processing. */ - bool removeFromHandlingQueueLocked(const std::string& messageId); - - /** - * Remove an @c AVSDirective from @c m_cancelingQueue. - * @note This method must only be called by threads that have acquired @c m_contextMutex. - * - * @param messageId The @c messageId of the @c AVSDirective to remove. - * @return Whether the @c AVSDirective was actually removed. - */ - bool removeFromCancelingQueueLocked(const std::string& messageId); - - /** - * Find the specified @c AVSDirective in the specified queue. - * - * @param messageId The message ID of the @c AVSDirective to find. - * @param queue The queue to search for the @c AVSDirective. - * @return An iterator positioned at the matching directive (or @c queue::end(), if not found). - */ - static std::deque>::iterator findDirectiveInQueueLocked( - const std::string& messageId, - std::deque>& queue); - - /** - * Remove the specified @c AVSDirective from the specified queue. If the queue is not empty after the - * removal, wake @c m_processingLoop. - * - * @param it An iterator positioned at the @c AVSDirective to remove from the queue. - * @param queue The queue to remove the @c AVSDirective from. - */ - void removeDirectiveFromQueueLocked( - std::deque>::iterator it, - std::deque>& queue); + void removeDirectiveLocked(std::shared_ptr directive); /** * Thread method for m_processingThread. @@ -197,9 +167,9 @@ class DirectiveProcessor { /** * Process (cancel) all the items in @c m_cancelingQueue. - * @note This method must only be called by threads that have acquired @c m_contextMutex. + * @note This method must only be called by threads that have acquired @c m_mutex. * - * @param lock A @c unique_lock on m_contextMutex from the callers context, allowing this method to release + * @param lock A @c unique_lock on m_mutex from the callers context, allowing this method to release * (and re-acquire) the lock around callbacks that need to be invoked. * @return Whether the @c AVSDirectives in @c m_cancelingQueue were processed. */ @@ -207,17 +177,36 @@ class DirectiveProcessor { /** * Process (handle) the next @c AVSDirective in @c m_handlingQueue. - * @note This method must only be called by threads that have acquired @c m_contextMutex. + * @note This method must only be called by threads that have acquired @c m_mutex. * - * @param lock A @c unique_lock on m_contextMutex from the callers context, allowing this method to release + * @param lock A @c unique_lock on m_mutex from the callers context, allowing this method to release * (and re-acquire) the lock around callbacks that need to be invoked. * @return Whether an @c AVSDirective from m_handlingQueue was processed. */ bool handleDirectiveLocked(std::unique_lock& lock); + /** + * Set the current @c dialogRequestId. This cancels the processing of any @c AVSDirectives with a non-empty + * dialogRequestId. + * @note This method must only be called by threads that have acquired @c m_mutex. + * + * @param dialogRequestId The new value for the current @c dialogRequestId. + */ + void setDialogRequestIdLocked(const std::string& dialogRequestId); + + /** + * Cancel the processing any @c AVSDirective with the specified dialogRequestId, and clear the m_dialogRequestID + * if it matches the specified dialogRequestId. + * @note This method must only be called by threads that have acquired @c m_mutex. + * + * @param dialogRequestId The dialogRequestId to scrub from processing. + */ + void scrubDialogRequestIdLocked(const std::string& dialogRequestId); + /** * Move all the directives being handled or queued for handling to @c m_cancelingQueue. Also reset the * current @c dialogRequestId. + * @note This method must only be called by threads that have acquired @c m_mutex. */ void queueAllDirectivesForCancellationLocked(); diff --git a/ADSL/include/ADSL/MessageInterpreter.h b/ADSL/include/ADSL/MessageInterpreter.h index b0dca7f245..1fbf8cdcc2 100644 --- a/ADSL/include/ADSL/MessageInterpreter.h +++ b/ADSL/include/ADSL/MessageInterpreter.h @@ -20,11 +20,9 @@ #include -#include #include #include #include -#include namespace alexaClientSDK { namespace adsl { diff --git a/ADSL/src/CMakeLists.txt b/ADSL/src/CMakeLists.txt index 6838798976..d981737eb6 100644 --- a/ADSL/src/CMakeLists.txt +++ b/ADSL/src/CMakeLists.txt @@ -8,7 +8,6 @@ add_library(ADSL SHARED DirectiveSequencer.cpp MessageInterpreter.cpp) target_include_directories(ADSL PUBLIC - "${ACL_SOURCE_DIR}/include" "${ADSL_SOURCE_DIR}/include" "${AVSCommon_INCLUDE_DIRS}") target_link_libraries(ADSL diff --git a/ADSL/src/DirectiveProcessor.cpp b/ADSL/src/DirectiveProcessor.cpp index 76c646f319..7857d81e56 100644 --- a/ADSL/src/DirectiveProcessor.cpp +++ b/ADSL/src/DirectiveProcessor.cpp @@ -63,13 +63,7 @@ DirectiveProcessor::~DirectiveProcessor() { void DirectiveProcessor::setDialogRequestId(const std::string& dialogRequestId) { std::lock_guard lock(m_mutex); - if (dialogRequestId == m_dialogRequestId) { - ACSDK_WARN(LX("setDialogRequestIdIgnored").d("reason", "unchanged").d("dialogRequestId", dialogRequestId)); - return; - } - ACSDK_INFO(LX("setDialogRequestId").d("dialogRequestId", dialogRequestId)); - queueAllDirectivesForCancellationLocked(); - m_dialogRequestId = dialogRequestId; + setDialogRequestIdLocked(dialogRequestId); } bool DirectiveProcessor::onDirective(std::shared_ptr directive) { @@ -84,13 +78,13 @@ bool DirectiveProcessor::onDirective(std::shared_ptr directive) { .d("messageId", directive->getMessageId()).d("action", "ignored").d("reason", "shuttingDown")); return false; } - if (directive->getDialogRequestId() != m_dialogRequestId) { + if (!directive->getDialogRequestId().empty() && directive->getDialogRequestId() != m_dialogRequestId) { ACSDK_INFO(LX("onDirective") - .d("messageId", directive->getMessageId()) - .d("action", "dropped") - .d("reason", "dialogRequestIdDoesNotMatch") - .d("directivesDialogRequestId", directive->getDialogRequestId()) - .d("dialogRequestId", m_dialogRequestId)); + .d("messageId", directive->getMessageId()) + .d("action", "dropped") + .d("reason", "dialogRequestIdDoesNotMatch") + .d("directivesDialogRequestId", directive->getDialogRequestId()) + .d("dialogRequestId", m_dialogRequestId)); return true; } m_directiveBeingPreHandled = directive; @@ -127,7 +121,7 @@ void DirectiveProcessor::shutdown() { DirectiveProcessor::DirectiveHandlerResult::DirectiveHandlerResult( DirectiveProcessor::ProcessorHandle processorHandle, std::shared_ptr directive) : - m_processorHandle{processorHandle}, m_messageId{directive->getMessageId()} { + m_processorHandle{processorHandle}, m_directive{directive} { } void DirectiveProcessor::DirectiveHandlerResult::setCompleted() { @@ -137,7 +131,7 @@ void DirectiveProcessor::DirectiveHandlerResult::setCompleted() { ACSDK_DEBUG(LX("setCompletedIgnored").d("reason", "directiveSequencerAlreadyShutDown")); return; } - it->second->onHandlingCompleted(m_messageId); + it->second->onHandlingCompleted(m_directive); } void DirectiveProcessor::DirectiveHandlerResult::setFailed(const std::string& description) { @@ -147,74 +141,52 @@ void DirectiveProcessor::DirectiveHandlerResult::setFailed(const std::string& de ACSDK_DEBUG(LX("setFailedIgnored").d("reason", "directiveSequencerAlreadyShutDown")); return; } - it->second->onHandlingFailed(m_messageId, description); + it->second->onHandlingFailed(m_directive, description); } -void DirectiveProcessor::onHandlingCompleted(const std::string& messageId) { +void DirectiveProcessor::onHandlingCompleted(std::shared_ptr directive) { std::lock_guard lock(m_mutex); - ACSDK_DEBUG(LX("onHandlingCompeted").d("messageId", messageId).d("directiveBeingPreHandled", + ACSDK_DEBUG(LX("onHandlingCompeted").d("messageId", directive->getMessageId()).d("directiveBeingPreHandled", m_directiveBeingPreHandled ? m_directiveBeingPreHandled->getMessageId() : "(nullptr)")); - if (m_directiveBeingPreHandled && m_directiveBeingPreHandled->getMessageId() == messageId) { - m_directiveBeingPreHandled.reset(); - } else if (!removeFromHandlingQueueLocked(messageId)) { - removeFromCancelingQueueLocked(messageId); - } + removeDirectiveLocked(directive); } -void DirectiveProcessor::onHandlingFailed(const std::string& messageId, const std::string& description) { +void DirectiveProcessor::onHandlingFailed(std::shared_ptr directive, const std::string& description) { std::unique_lock lock(m_mutex); ACSDK_DEBUG(LX("onHandlingFailed") - .d("messageId", messageId) + .d("messageId", directive->getMessageId()) .d("directiveBeingPreHandled", m_directiveBeingPreHandled ? m_directiveBeingPreHandled->getMessageId() : "(nullptr)") .d("description", description)); - if (m_directiveBeingPreHandled && m_directiveBeingPreHandled->getMessageId() == messageId) { - m_directiveBeingPreHandled.reset(); - queueAllDirectivesForCancellationLocked(); - } else if (removeFromHandlingQueueLocked(messageId)) { - queueAllDirectivesForCancellationLocked(); - } else { - removeFromCancelingQueueLocked(messageId); - } + removeDirectiveLocked(directive); + scrubDialogRequestIdLocked(directive->getDialogRequestId()); } -bool DirectiveProcessor::removeFromHandlingQueueLocked(const std::string& messageId) { - auto it = findDirectiveInQueueLocked(messageId, m_handlingQueue); - if (m_handlingQueue.end() == it) { - return false; - } - if (m_isHandlingDirective && m_handlingQueue.begin() == it) { - m_isHandlingDirective = false; +void DirectiveProcessor::removeDirectiveLocked(std::shared_ptr directive) { + auto matches = [directive](std::shared_ptr item) { + return item == directive; + }; + + m_cancelingQueue.erase( + std::remove_if(m_cancelingQueue.begin(), m_cancelingQueue.end(), matches), + m_cancelingQueue.end()); + + if (matches(m_directiveBeingPreHandled)) { + m_directiveBeingPreHandled.reset(); } - removeDirectiveFromQueueLocked(it, m_handlingQueue); - return true; -} -bool DirectiveProcessor::removeFromCancelingQueueLocked(const std::string& messageId) { - auto it = findDirectiveInQueueLocked(messageId, m_cancelingQueue); - if (m_cancelingQueue.end() == it) { - return false; + if (m_isHandlingDirective && !m_handlingQueue.empty() && matches(m_handlingQueue.front())) { + m_isHandlingDirective = false; + m_handlingQueue.pop_front(); } - removeDirectiveFromQueueLocked(it, m_cancelingQueue); - return true; -} -std::deque>::iterator DirectiveProcessor::findDirectiveInQueueLocked( - const std::string& messageId, - std::deque>& queue) { - auto matches = [messageId](std::shared_ptr element) { - return element->getMessageId() == messageId; - }; - return std::find_if(queue.begin(), queue.end(), matches); -} + m_handlingQueue.erase( + std::remove_if(m_handlingQueue.begin(), m_handlingQueue.end(), matches), + m_handlingQueue.end()); -void DirectiveProcessor::removeDirectiveFromQueueLocked( - std::deque>::iterator it, - std::deque>& queue) { - queue.erase(it); - if (!queue.empty()) { + if (!m_cancelingQueue.empty() || !m_handlingQueue.empty()) { m_wakeProcessingLoop.notify_one(); } } @@ -271,11 +243,77 @@ bool DirectiveProcessor::handleDirectiveLocked(std::unique_lock &loc } } if (!handled) { - queueAllDirectivesForCancellationLocked(); + scrubDialogRequestIdLocked(directive->getDialogRequestId()); } return true; } +void DirectiveProcessor::setDialogRequestIdLocked(const std::string& dialogRequestId) { + if (dialogRequestId == m_dialogRequestId) { + ACSDK_WARN(LX("setDialogRequestIdLockedIgnored").d("reason", "unchanged").d("dialogRequestId", dialogRequestId)); + return; + } + ACSDK_INFO(LX("setDialogRequestIdLocked").d("oldValue", m_dialogRequestId).d("newValue", dialogRequestId)); + scrubDialogRequestIdLocked(m_dialogRequestId); + m_dialogRequestId = dialogRequestId; +} + +void DirectiveProcessor::scrubDialogRequestIdLocked(const std::string& dialogRequestId) { + if (dialogRequestId.empty()) { + ACSDK_DEBUG(LX("scrubDialogRequestIdLocked").d("reason", "emptyDialogRequestId")); + return; + } + + ACSDK_DEBUG(LX("scrubDialogRequestIdLocked").d("dialogRequestId", dialogRequestId)); + + bool changed = false; + + // If a matching directive is in the midst of a preHandleDirective() call (i.e. before the + // directive is added to the m_handlingQueue) queue it for canceling instead. + if (m_directiveBeingPreHandled) { + auto id = m_directiveBeingPreHandled->getDialogRequestId(); + if (!id.empty() && id == dialogRequestId) { + m_cancelingQueue.push_back(m_directiveBeingPreHandled); + m_directiveBeingPreHandled.reset(); + changed = true; + } + } + + // If a mathcing directive in the midst of a handleDirective() call, reset m_isHandlingDirective + // so we won't block processing subsequent directives. This directive is already in m_handlingQueue + // and will be moved to m_cancelingQueue, below. + if (m_isHandlingDirective && !m_handlingQueue.empty()) { + auto id = m_handlingQueue.front()->getDialogRequestId(); + if (!id.empty() && id == dialogRequestId) { + m_isHandlingDirective = false; + changed = true; + } + } + + // Filter matching directives from m_handlingQueue and put them in m_cancelingQueue. + std::deque> temp; + for (auto directive : m_handlingQueue) { + auto id = directive->getDialogRequestId(); + if (!id.empty() && id == dialogRequestId) { + m_cancelingQueue.push_back(directive); + changed = true; + } else { + temp.push_back(directive); + } + } + std::swap(temp, m_handlingQueue); + + // If the dialogRequestId to scrub is the current value, reset the current value. + if (dialogRequestId == m_dialogRequestId) { + m_dialogRequestId.clear(); + } + + // If there were any changes, wake up the processing loop. + if (changed) { + m_wakeProcessingLoop.notify_one(); + } +} + void DirectiveProcessor::queueAllDirectivesForCancellationLocked() { m_dialogRequestId.clear(); if (m_directiveBeingPreHandled) { diff --git a/ADSL/src/DirectiveSequencer.cpp b/ADSL/src/DirectiveSequencer.cpp index f6158c7072..ad8b400720 100644 --- a/ADSL/src/DirectiveSequencer.cpp +++ b/ADSL/src/DirectiveSequencer.cpp @@ -130,12 +130,24 @@ void DirectiveSequencer::receiveDirectiveLocked(std::unique_lock &lo auto directive = m_receivingQueue.front(); m_receivingQueue.pop_front(); lock.unlock(); + bool handled = false; + + /** + * Previously it was expected that all directives resulting from a Recognize event + * would be tagged with the dialogRequestId of that event. In practice that is not + * the observed behavior. + */ +#ifdef DIALOG_REQUST_ID_IN_ALL_RESPONSE_DIRECTIVES if (directive->getDialogRequestId().empty()) { handled = m_directiveRouter.handleDirectiveImmediately(directive); - } else { + } else { handled = m_directiveProcessor->onDirective(directive); } +#else + handled = m_directiveProcessor->onDirective(directive); +#endif + if (!handled) { ACSDK_INFO(LX("sendingExceptionEncountered").d("messageId", directive->getMessageId())); m_exceptionSender->sendExceptionEncountered( diff --git a/ADSL/src/MessageInterpreter.cpp b/ADSL/src/MessageInterpreter.cpp index 758c9d08ed..39df46e798 100644 --- a/ADSL/src/MessageInterpreter.cpp +++ b/ADSL/src/MessageInterpreter.cpp @@ -32,7 +32,6 @@ using namespace avsCommon::avs; using namespace avsCommon::avs::attachment; using namespace avsCommon::sdkInterfaces; using namespace avsCommon::utils::json::jsonUtils; -using namespace acl; using namespace rapidjson; /// String to identify log entries originating from this file. diff --git a/ADSL/test/DirectiveSequencerTest.cpp b/ADSL/test/DirectiveSequencerTest.cpp index 2ceb28c1a1..b2ff71a6ac 100644 --- a/ADSL/test/DirectiveSequencerTest.cpp +++ b/ADSL/test/DirectiveSequencerTest.cpp @@ -225,9 +225,9 @@ TEST_F(DirectiveSequencerTest, testEmptyDialogRequestId) { DirectiveHandlerConfiguration config; config[{NAMESPACE_SPEAKER, NAME_SET_VOLUME}] = BlockingPolicy::NON_BLOCKING; auto handler = MockDirectiveHandler::create(config); - EXPECT_CALL(*(handler.get()), handleDirectiveImmediately(directive)).Times(1); - EXPECT_CALL(*(handler.get()), preHandleDirective(_, _)).Times(0); - EXPECT_CALL(*(handler.get()), handleDirective(_)).Times(0); + EXPECT_CALL(*(handler.get()), handleDirectiveImmediately(directive)).Times(0); + EXPECT_CALL(*(handler.get()), preHandleDirective(_, _)).Times(1); + EXPECT_CALL(*(handler.get()), handleDirective(_)).Times(1); EXPECT_CALL(*(handler.get()), cancelDirective(_)).Times(0); ASSERT_TRUE(m_sequencer->addDirectiveHandler(handler)); m_sequencer->onDirective(directive); @@ -269,7 +269,8 @@ TEST_F(DirectiveSequencerTest, testRemovingAndChangingHandlers) { EXPECT_CALL(*(handler1.get()), handleDirective(_)).Times(0); EXPECT_CALL(*(handler1.get()), cancelDirective(_)).Times(0); - EXPECT_CALL(*(handler2.get()), handleDirectiveImmediately(directive1)).Times(1); + EXPECT_CALL(*(handler2.get()), preHandleDirective(directive1, _)).Times(1); + EXPECT_CALL(*(handler2.get()), handleDirective(MESSAGE_ID_1)).Times(1); ASSERT_TRUE(m_sequencer->addDirectiveHandler(handler0)); ASSERT_TRUE(m_sequencer->addDirectiveHandler(handler1)); @@ -338,9 +339,9 @@ TEST_F(DirectiveSequencerTest, testBlockingThenNonDialogDirective) { EXPECT_CALL(*(handler0.get()), handleDirective(MESSAGE_ID_0)).Times(1); EXPECT_CALL(*(handler0.get()), cancelDirective(_)).Times(1); - EXPECT_CALL(*(handler1.get()), handleDirectiveImmediately(directive1)).Times(1); - EXPECT_CALL(*(handler1.get()), preHandleDirective(_, _)).Times(0); - EXPECT_CALL(*(handler1.get()), handleDirective(_)).Times(0); + EXPECT_CALL(*(handler1.get()), handleDirectiveImmediately(directive1)).Times(0); + EXPECT_CALL(*(handler1.get()), preHandleDirective(_, _)).Times(1); + EXPECT_CALL(*(handler1.get()), handleDirective(_)).Times(1); EXPECT_CALL(*(handler1.get()), cancelDirective(_)).Times(0); ASSERT_TRUE(m_sequencer->addDirectiveHandler(handler0)); @@ -349,7 +350,7 @@ TEST_F(DirectiveSequencerTest, testBlockingThenNonDialogDirective) { m_sequencer->setDialogRequestId(DIALOG_REQUEST_ID_0); m_sequencer->onDirective(directive0); m_sequencer->onDirective(directive1); - ASSERT_TRUE(handler1->waitUntilHandling()); + ASSERT_TRUE(handler1->waitUntilPreHandling()); ASSERT_TRUE(handler0->waitUntilHandling()); m_sequencer->setDialogRequestId(DIALOG_REQUEST_ID_1); ASSERT_TRUE(handler0->waitUntilCanceling()); diff --git a/ADSL/test/MessageInterpreterTest.cpp b/ADSL/test/MessageInterpreterTest.cpp index 60455cc115..8ee505b4cf 100644 --- a/ADSL/test/MessageInterpreterTest.cpp +++ b/ADSL/test/MessageInterpreterTest.cpp @@ -32,7 +32,6 @@ namespace adsl { namespace test { using namespace ::testing; -using namespace alexaClientSDK::acl; using namespace alexaClientSDK::avsCommon; using namespace alexaClientSDK::avsCommon::avs; using namespace alexaClientSDK::avsCommon::avs::attachment; diff --git a/AVSCommon/AVS/include/AVSCommon/AVS/MessageRequest.h b/AVSCommon/AVS/include/AVSCommon/AVS/MessageRequest.h index 05cf25ba8b..cd89f05a6f 100644 --- a/AVSCommon/AVS/include/AVSCommon/AVS/MessageRequest.h +++ b/AVSCommon/AVS/include/AVSCommon/AVS/MessageRequest.h @@ -46,6 +46,9 @@ class MessageRequest { /// The send failed because AVS was not connected. NOT_CONNECTED, + /// The send failed because AVS is not synchronized. + NOT_SYNCHRONIZED, + /// The send failed because of timeout waiting for AVS response. TIMEDOUT, diff --git a/AVSCommon/AVS/src/DialogUXStateAggregator.cpp b/AVSCommon/AVS/src/DialogUXStateAggregator.cpp index 619c4e6f90..8f7ce24315 100644 --- a/AVSCommon/AVS/src/DialogUXStateAggregator.cpp +++ b/AVSCommon/AVS/src/DialogUXStateAggregator.cpp @@ -106,6 +106,9 @@ void DialogUXStateAggregator::onStateChanged(SpeechSynthesizerObserver::SpeechSy setState(DialogUXStateObserverInterface::DialogUXState::SPEAKING); return; case SpeechSynthesizerObserver::SpeechSynthesizerState::FINISHED: + if (DialogUXStateObserverInterface::DialogUXState::SPEAKING != m_currentState) { + return; + } if (!m_multiturnSpeakingToListeningTimer.start( SHORT_TIMEOUT, std::bind( &DialogUXStateAggregator::transitionFromSpeakingFinished, this)).valid()) { diff --git a/AVSCommon/AVS/src/MessageRequest.cpp b/AVSCommon/AVS/src/MessageRequest.cpp index 7ce0c2b38a..89e01a5b2b 100644 --- a/AVSCommon/AVS/src/MessageRequest.cpp +++ b/AVSCommon/AVS/src/MessageRequest.cpp @@ -54,6 +54,8 @@ std::string MessageRequest::statusToString(Status status) { return "SUCCESS"; case Status::NOT_CONNECTED: return "NOT_CONNECTED"; + case Status::NOT_SYNCHRONIZED: + return "NOT_SYNCHRONIZED"; case Status::TIMEDOUT: return "TIMEDOUT"; case Status::PROTOCOL_ERROR: diff --git a/AVSCommon/CMakeLists.txt b/AVSCommon/CMakeLists.txt index d2ccde316a..b0a085cde6 100644 --- a/AVSCommon/CMakeLists.txt +++ b/AVSCommon/CMakeLists.txt @@ -34,6 +34,7 @@ add_library(AVSCommon SHARED Utils/src/Logger/LogEntryBuffer.cpp Utils/src/Logger/LogEntryStream.cpp Utils/src/Logger/Logger.cpp + Utils/src/Logger/LoggerSinkManager.cpp Utils/src/Logger/LoggerUtils.cpp Utils/src/Logger/ModuleLogger.cpp Utils/src/Logger/ThreadMoniker.cpp diff --git a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/SpeechSynthesizerObserver.h b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/SpeechSynthesizerObserver.h index 192fa43522..380d1fbeb9 100644 --- a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/SpeechSynthesizerObserver.h +++ b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/SpeechSynthesizerObserver.h @@ -18,6 +18,8 @@ #ifndef ALEXA_CLIENT_SDK_AVS_COMMON_SDK_INTERFACES_INCLUDE_AVS_COMMON_SDK_INTERFACES_SPEECH_SYNTHESIZER_OBSERVER_H_ #define ALEXA_CLIENT_SDK_AVS_COMMON_SDK_INTERFACES_INCLUDE_AVS_COMMON_SDK_INTERFACES_SPEECH_SYNTHESIZER_OBSERVER_H_ +#include + namespace alexaClientSDK { namespace avsCommon { namespace sdkInterfaces { @@ -50,6 +52,25 @@ class SpeechSynthesizerObserver { virtual void onStateChanged(SpeechSynthesizerState state) = 0; }; +/** + * Write a @c State value to an @c ostream as a string. + * + * @param stream The stream to write the value to. + * @param state The state value to write to the @c ostream as a string. + * @return The @c ostream that was passed in and written to. + */ +inline std::ostream& operator<<(std::ostream& stream, const SpeechSynthesizerObserver::SpeechSynthesizerState state) { + switch(state) { + case SpeechSynthesizerObserver::SpeechSynthesizerState::PLAYING: + stream << "PLAYING"; + break; + case SpeechSynthesizerObserver::SpeechSynthesizerState::FINISHED: + stream << "FINISHED"; + break; + } + return stream; +} + } // namespace sdkInterfaces } // namespace avsCommon } // namespace alexaClientSDK diff --git a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/StateSynchronizerObserverInterface.h b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/StateSynchronizerObserverInterface.h new file mode 100644 index 0000000000..7fe6eeec0a --- /dev/null +++ b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/StateSynchronizerObserverInterface.h @@ -0,0 +1,62 @@ +/* + * StateSynchronizerObserverInterface.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_AVS_COMMON_SDK_INTERFACES_INCLUDE_AVS_COMMON_SDK_INTERFACES_STATE_SYNCHRONIZER_OBSERVER_INTERFACE_H_ +#define ALEXA_CLIENT_SDK_AVS_COMMON_SDK_INTERFACES_INCLUDE_AVS_COMMON_SDK_INTERFACES_STATE_SYNCHRONIZER_OBSERVER_INTERFACE_H_ + +namespace alexaClientSDK { +namespace avsCommon { +namespace sdkInterfaces { + +/** + * This interface provides a callback that signals state has been synchronized successfully. Since @c SynchronizeState + * event should be the first message sent to AVS upon connection, if a component is sending a message, then it needs to + * know the state of @c StateSynchronizer in order to start sending, and therefore contain an implementation of this + * interface. Moreover, said component or implementation should add themselves to @c StateSynchronizer to receive the + * callback. + */ +class StateSynchronizerObserverInterface { +public: + /** + * This enum provides the state of the @c StateSynchronizer. + */ + enum class State { + /// The state in which @c StateSynchronizer has not send @c SynchronizeState event. + NOT_SYNCHRONIZED, + /// The state in which the state has been synchronized. + SYNCHRONIZED + }; + + /** + * Destructor. + */ + virtual ~StateSynchronizerObserverInterface() = default; + + /** + * Get the notification that the state has been synchronized. + * + * @param newState The state to which the @c StateSynchronizer has transitioned. + * @note The implementation of this function should return fast in order not to block the component that calls it. + */ + virtual void onStateChanged(State newState) = 0; +}; + +} // namespace sdkInterfaces +} // namespace avsCommon +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AVS_COMMON_SDK_INTERFACES_INCLUDE_AVS_COMMON_SDK_INTERFACES_STATE_SYNCHRONIZER_OBSERVER_INTERFACE_H_ diff --git a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/UserActivityNotifierInterface.h b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/UserActivityNotifierInterface.h new file mode 100644 index 0000000000..681c665492 --- /dev/null +++ b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/UserActivityNotifierInterface.h @@ -0,0 +1,43 @@ +/* + * UserActivityNotifierInterface.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_AVS_COMMON_SDK_INTERFACES_INCLUDE_AVS_COMMON_SDK_INTERFACES_USER_ACTIVITY_NOTIFIER_INTERFACE_H_ +#define ALEXA_CLIENT_SDK_AVS_COMMON_SDK_INTERFACES_INCLUDE_AVS_COMMON_SDK_INTERFACES_USER_ACTIVITY_NOTIFIER_INTERFACE_H_ + +namespace alexaClientSDK { +namespace avsCommon { +namespace sdkInterfaces { + +/** + * This interface is used to notify an implementation of the user activity. Any component that interacts with the user + * (e.g. AudioInputProcessor) should register an instance of this interface to signal when user interaction is detected + * (e.g. SpeechStarted). + */ +class UserActivityNotifierInterface { +public: + /// Destructor. + virtual ~UserActivityNotifierInterface() = default; + + /// The function to be called when the user has become active. + virtual void onUserActive() = 0; +}; + +} // namespace sdkInterfaces +} // namespace avsCommon +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AVS_COMMON_SDK_INTERFACES_INCLUDE_AVS_COMMON_SDK_INTERFACES_USER_ACTIVITY_NOTIFIER_INTERFACE_H_ diff --git a/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockStateSynchronizerObserver.h b/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockStateSynchronizerObserver.h new file mode 100644 index 0000000000..4683442483 --- /dev/null +++ b/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockStateSynchronizerObserver.h @@ -0,0 +1,42 @@ +/* + * MockStateSynchronizerObserver.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_AVS_COMMON_SDK_INTERFACES_TEST_AVS_COMMON_SDK_INTERFACES_MOCK_STATE_SYNCHRONIZER_OBSERVER_H_ +#define ALEXA_CLIENT_SDK_AVS_COMMON_SDK_INTERFACES_TEST_AVS_COMMON_SDK_INTERFACES_MOCK_STATE_SYNCHRONIZER_OBSERVER_H_ + +#include "AVSCommon/SDKInterfaces/StateSynchronizerObserverInterface.h" +#include + +namespace alexaClientSDK { +namespace avsCommon { +namespace sdkInterfaces { +namespace test { + +/** + * This interface provides a callback that signals state has been synchronized successfully. + */ +class MockStateSynchronizerObserver : public StateSynchronizerObserverInterface { +public: + MOCK_METHOD1(onStateChanged, void(StateSynchronizerObserverInterface::State newstate)); +}; + +} // namespace test +} // namespace sdkInterfaces +} // namespace avsCommon +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AVS_COMMON_SDK_INTERFACES_TEST_AVS_COMMON_SDK_INTERFACES_MOCK_STATE_SYNCHRONIZER_OBSERVER_H_ diff --git a/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockUserActivityNotifier.h b/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockUserActivityNotifier.h new file mode 100644 index 0000000000..9595d2406e --- /dev/null +++ b/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockUserActivityNotifier.h @@ -0,0 +1,40 @@ +/* + * MockUserActivityNotifier.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_AVS_COMMON_SDK_INTERFACES_TEST_AVS_COMMON_SDK_INTERFACES_MOCK_USER_ACTIVITY_NOTIFIER_H_ +#define ALEXA_CLIENT_SDK_AVS_COMMON_SDK_INTERFACES_TEST_AVS_COMMON_SDK_INTERFACES_MOCK_USER_ACTIVITY_NOTIFIER_H_ + +#include "AVSCommon/SDKInterfaces/UserActivityNotifierInterface.h" +#include + +namespace alexaClientSDK { +namespace avsCommon { +namespace sdkInterfaces { +namespace test { + +/// Mock class that implements @c UserActivityNotifierInterface. +class MockUserActivityNotifier : public UserActivityNotifierInterface { +public: + MOCK_METHOD0(onUserActive, void()); +}; + +} // namespace test +} // namespace sdkInterfaces +} // namespace avsCommon +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AVS_COMMON_SDK_INTERFACES_TEST_AVS_COMMON_SDK_INTERFACES_MOCK_USER_ACTIVITY_NOTIFIER_H_ diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/Logger/Logger.h b/AVSCommon/Utils/include/AVSCommon/Utils/Logger/Logger.h index cb16bc0656..8925e01475 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/Logger/Logger.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/Logger/Logger.h @@ -29,6 +29,7 @@ #include "AVSCommon/Utils/Logger/Level.h" #include "AVSCommon/Utils/Logger/LogEntry.h" #include "AVSCommon/Utils/Logger/LogLevelObserverInterface.h" +#include "AVSCommon/Utils/Logger/SinkObserverInterface.h" /** * Inner part of ACSDK_STRINGIFY. Turns an expression in to a string literal. diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/Logger/LoggerSinkManager.h b/AVSCommon/Utils/include/AVSCommon/Utils/Logger/LoggerSinkManager.h new file mode 100644 index 0000000000..742df7947e --- /dev/null +++ b/AVSCommon/Utils/include/AVSCommon/Utils/Logger/LoggerSinkManager.h @@ -0,0 +1,88 @@ +/* + * LoggerSinkManager.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_AVS_COMMON_UTILS_INCLUDE_AVS_COMMON_UTILS_LOGGER_LOGGER_SINK_MANAGER_H_ +#define ALEXA_CLIENT_SDK_AVS_COMMON_UTILS_INCLUDE_AVS_COMMON_UTILS_LOGGER_LOGGER_SINK_MANAGER_H_ + +#include +#include +#include + +#include "AVSCommon/Utils/Logger/Logger.h" + +namespace alexaClientSDK { +namespace avsCommon { +namespace utils { +namespace logger { + +/** + * A manager to manage the sink logger and notify SinkObservers of any changes. + */ +class LoggerSinkManager { +public: + /** + * Return the one and only @c LoggerSinkManager instance. + * + * @return The one and only @c LoggerSinkManager instance. + */ + static LoggerSinkManager& instance(); + + /** + * Add a SinkObserver to the manager. + * + * @param observer The @c SinkObserverInterface be be added. + */ + void addSinkObserver(SinkObserverInterface* observer); + + /** + * Remove a SinkObserver from the manager. + * + * @param observer The @c SinkObserverInterface to be removed. + */ + void removeSinkObserver(SinkObserverInterface* observer); + + /** + * Change the sink logger managed by the manager. + * + * @param sink The new @c Logger to forward logs to. + * + * @note It is up to the application to serialize calls to changeSinkLogger. + */ + void changeSinkLogger(Logger& sink); + +private: + /** + * Constructor. + */ + LoggerSinkManager(); + + /// This mutex guards access to m_sinkObservers. + std::mutex m_sinkObserverMutex; + + /// Vector of SinkObservers to be managed. + std::vector m_sinkObservers; + + /// The @c Logger to forward logs to. + std::atomic m_sink; +}; + +} // namespace logger +} // namespace utils +} // namespace avsCommon +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AVS_COMMON_UTILS_INCLUDE_AVS_COMMON_UTILS_LOGGER_LOGGER_SINK_MANAGER_H_ diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/Logger/ModuleLogger.h b/AVSCommon/Utils/include/AVSCommon/Utils/Logger/ModuleLogger.h index 447ccf3534..944f7782c9 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/Logger/ModuleLogger.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/Logger/ModuleLogger.h @@ -28,16 +28,15 @@ namespace logger { /** * @c Logger implementation providing per module configuration. Forwards logs to another @c Logger. */ -class ModuleLogger : public Logger, protected LogLevelObserverInterface { +class ModuleLogger : public Logger, protected LogLevelObserverInterface, protected SinkObserverInterface { public: /** * Constructor. * * @param configKey The name of the root configuration key to inspect for a "logLevel" string value. That * string is used to specify the lowest log severity level that this @c ModuleLogger should emit. - * @param sink The @c Logger to forwards logs to. */ - ModuleLogger(const std::string& configKey, Logger& sink = ACSDK_GET_SINK_LOGGER()); + ModuleLogger(const std::string& configKey); void setLevel(Level level) override; @@ -50,12 +49,14 @@ class ModuleLogger : public Logger, protected LogLevelObserverInterface { private: void onLogLevelChanged(Level level) override; + void onSinkChanged(Logger& sink) override; + /// flag to determine if the m_sink's logLevel is to be used bool m_useSinkLogLevel; protected: /// The @c Logger to forward logs to. - Logger& m_sink; + std::atomic m_sink; }; } // namespace logger diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/Logger/SinkObserverInterface.h b/AVSCommon/Utils/include/AVSCommon/Utils/Logger/SinkObserverInterface.h new file mode 100644 index 0000000000..51ab0e1d3d --- /dev/null +++ b/AVSCommon/Utils/include/AVSCommon/Utils/Logger/SinkObserverInterface.h @@ -0,0 +1,47 @@ +/* + * SinkObserverInterface.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_AVS_COMMON_UTILS_INCLUDE_AVS_COMMON_UTILS_LOGGER_SINK_OBSERVER_INTERFACE_H_ +#define ALEXA_CLIENT_SDK_AVS_COMMON_UTILS_INCLUDE_AVS_COMMON_UTILS_LOGGER_SINK_OBSERVER_INTERFACE_H_ + +namespace alexaClientSDK { +namespace avsCommon { +namespace utils { +namespace logger { + +// forward declaration +class Logger; + +/** + * This interface class allows notifications when the sink Logger changes. + */ +class SinkObserverInterface { +public: + /** + * This function will be called when the sink changes. + * + * @param sink The updated sink @c Logger + */ + virtual void onSinkChanged(Logger& sink) = 0; +}; + +} // namespace logger +} // namespace utils +} // namespace avsCommon +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AVS_COMMON_UTILS_INCLUDE_AVS_COMMON_UTILS_LOGGER_SINK_OBSERVER_INTERFACE_H_ diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/MediaPlayer/MediaPlayerInterface.h b/AVSCommon/Utils/include/AVSCommon/Utils/MediaPlayer/MediaPlayerInterface.h index 926501ea0c..c56ea648af 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/MediaPlayer/MediaPlayerInterface.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/MediaPlayer/MediaPlayerInterface.h @@ -19,6 +19,7 @@ #define ALEXA_CLIENT_SDK_AVS_COMMON_UTILS_INCLUDE_AVS_COMMON_UTILS_MEDIA_PLAYER_MEDIA_PLAYER_INTERFACE_H_ #include +#include #include #include "AVSCommon/AVS/Attachment/AttachmentReader.h" @@ -65,7 +66,20 @@ class MediaPlayerInterface { * stopping the player, this will return @c FAILURE. */ virtual MediaPlayerStatus setSource( - std::unique_ptr attachmentReader) = 0; + std::shared_ptr attachmentReader) = 0; + + /** + * Set the source to play. The source should be set before issuing @c play or @c stop. + * + * The @c MediaPlayer can handle only one source at a time. + * + * @param url The url to set as the source. + * + * @return @c SUCCESS if the source was set successfully else @c FAILURE. If setSource is called when audio is + * currently playing, the playing audio will be stopped and the source set to the new value. If there is an error + * stopping the player, this will return @c FAILURE. + */ + virtual MediaPlayerStatus setSource(const std::string& url) = 0; /** * Set the source to play. The source should be set before issuing @c play or @c stop. @@ -80,7 +94,12 @@ class MediaPlayerInterface { * stopping the player, this will return @c FAILURE. */ virtual MediaPlayerStatus setSource( - std::unique_ptr stream, bool repeat) = 0; + std::shared_ptr stream, bool repeat) = 0; + + /** + * TODO ACSDK-423: Implement setOffset behavior. + */ + virtual MediaPlayerStatus setOffset(std::chrono::milliseconds offset) { return MediaPlayerStatus::FAILURE; } /** * Start playing audio. The source should be set before issuing @c play. If @c play is called without @@ -106,6 +125,31 @@ class MediaPlayerInterface { */ virtual MediaPlayerStatus stop() = 0; + /** + * Pause playing the audio. Once audio has been paused, calling @c resume() will start the audio. + * The source should be set before issuing @c pause. If @c pause is called without setting source, it will + * return an error. + * Calling @c pause will only have an effect when audio is currently playing. Calling @c pause in all other states will have no effect, + * and result in a return of @c FAILURE. + * + * @return @c SUCCESS if the state transition to pause was successful. If state transition is pending then it returns + * @c PENDING and the state transition status is notified via @c onPlaybackPaused or @c onPlaybackError. If state + * transition was unsuccessful, returns @c FAILURE. + */ + virtual MediaPlayerStatus pause() = 0; + + /** + * Resume playing the paused audio. The source should be set before issuing @c resume. If @c resume is called without setting source, it will + * return an error. + * Calling @c resume will only have an effect when audio is currently paused. Calling @c resume in other states will have no effect, + * and result in a return of @c FAILURE. + * + * @return @c SUCCESS if the state transition to play was successful. If state transition is pending then it returns + * @c PENDING and the state transition status is notified via @c onPlaybackResumed or @c onPlaybackError. If state + * transition was unsuccessful, returns @c FAILURE. + */ + virtual MediaPlayerStatus resume() = 0; + /** * Returns the offset, in milliseconds, of the media stream. * diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/MediaPlayer/MediaPlayerObserverInterface.h b/AVSCommon/Utils/include/AVSCommon/Utils/MediaPlayer/MediaPlayerObserverInterface.h index 0446b1a206..bf6420b740 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/MediaPlayer/MediaPlayerObserverInterface.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/MediaPlayer/MediaPlayerObserverInterface.h @@ -60,6 +60,40 @@ class MediaPlayerObserverInterface { * or playback. */ virtual void onPlaybackError(std::string error) = 0; + + /** + * This is an indication to the observer that the @c MediaPlayer has paused playing the audio data. + * + * @note The observer has to return soon. Otherwise this could block the @c MediaPlayer from processing other signals + * or playback. + */ + virtual void onPlaybackPaused() {}; + + /** + * This is an indication to the observer that the @c MediaPlayer has resumed playing the audio data. + * + * @note The observer has to return soon. Otherwise this could block the @c MediaPlayer from processing other signals + * or playback. + */ + virtual void onPlaybackResumed() {}; + + /** + * This is an indication to the observer that the @c MediaPlayer is experiencing a buffer underrun. + * This will only be sent after playback has started. Playback will be paused until the buffer is filled. + * + * @note The observer has to return soon. Otherwise this could block the @c MediaPlayer from processing other signals + * or playback. + */ + virtual void onBufferUnderrun() {} + + /** + * This is an indication to the observer that the @c MediaPlayer's buffer has refilled. This will only be sent after playback + * has started. Playback will resume. + * + * @note The observer has to return soon. Otherwise this could block the @c MediaPlayer from processing other signals + * or playback. + */ + virtual void onBufferRefilled() {} }; } // namespace mediaPlayer diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/PlaylistParser/PlaylistParserInterface.h b/AVSCommon/Utils/include/AVSCommon/Utils/PlaylistParser/PlaylistParserInterface.h new file mode 100644 index 0000000000..52cc0becbd --- /dev/null +++ b/AVSCommon/Utils/include/AVSCommon/Utils/PlaylistParser/PlaylistParserInterface.h @@ -0,0 +1,59 @@ +/* + * PlaylistParserInterface.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_AVS_COMMON_UTILS_INCLUDE_AVS_COMMON_UTILS_PLAYLIST_PARSER_PLAYLIST_PARSER_INTERFACE_H_ +#define ALEXA_CLIENT_SDK_AVS_COMMON_UTILS_INCLUDE_AVS_COMMON_UTILS_PLAYLIST_PARSER_PLAYLIST_PARSER_INTERFACE_H_ + +#include +#include + +#include "PlaylistParserObserverInterface.h" + +namespace alexaClientSDK { +namespace avsCommon { +namespace utils { +namespace playlistParser { + +/** + * A PlaylistParser parses playlists. + */ +class PlaylistParserInterface { +public: + /** + * Destructor. + */ + virtual ~PlaylistParserInterface() = default; + + /** + * This function returns immediately. It parses the playlist specified in the @c url asynchronously. The playlist + * parsing is a DFS parsing. If the playlist contains a link to another playlist, then it will proceed to parse + * that before proceeding. The result of parsing the playlist is notified to the + * @c PlaylistParserObserverInterface via the @c onPlaylistParsed call. + * + * @param url The url of the playlist to be parsed. + * @param observer The observer to be notified of when playlist parsing is complete. + * @return @c true if it was successful in adding a new playlist parsing request to the queue @c else false. + */ + virtual bool parsePlaylist(const std::string& url, std::shared_ptr observer) = 0; +}; + +} // namespace playlistParser +} // namespace utils +} // namespace avsCommon +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AVS_COMMON_UTILS_INCLUDE_AVS_COMMON_UTILS_PLAYLIST_PARSER_PLAYLIST_PARSER_INTERFACE_H_ diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/PlaylistParser/PlaylistParserObserverInterface.h b/AVSCommon/Utils/include/AVSCommon/Utils/PlaylistParser/PlaylistParserObserverInterface.h new file mode 100644 index 0000000000..f97bf91754 --- /dev/null +++ b/AVSCommon/Utils/include/AVSCommon/Utils/PlaylistParser/PlaylistParserObserverInterface.h @@ -0,0 +1,106 @@ +/* + * PlaylistParserObserverInterface.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_AVS_COMMON_UTILS_INCLUDE_AVS_COMMON_UTILS_PLAYLIST_PARSER_PLAYLIST_PARSER_OBSERVER_INTERFACE_H_ +#define ALEXA_CLIENT_SDK_AVS_COMMON_UTILS_INCLUDE_AVS_COMMON_UTILS_PLAYLIST_PARSER_PLAYLIST_PARSER_OBSERVER_INTERFACE_H_ + +#include +#include + +namespace alexaClientSDK { +namespace avsCommon { +namespace utils { +namespace playlistParser { + +/** + * An enum class used to specify the result of a parsing operation. + */ +enum class PlaylistParseResult { + + /// The playlist was parsed successfully. + PARSE_RESULT_SUCCESS, + + /// The playlist could not be handled. + PARSE_RESULT_UNHANDLED, + + /// There was an error parsing the playlist. + PARSE_RESULT_ERROR, + + /// The playlist was ignored due to its scheme or MIME type. + PARSE_RESULT_IGNORED, + + ///Parsing of the playlist was cancelled part-way through. + PARSE_RESULT_CANCELLED +}; + +/** + * An observer of the playlist parser. + */ +class PlaylistParserObserverInterface { +public: + /** + * Destructor. + */ + virtual ~PlaylistParserObserverInterface() = default; + + /** + * Notification that the playlist parsing has been completed. + * + * @param playlistUrl The playlist that was parsed. + * @param urls A list of the urls extracted from the playlist. + * @param parseResult The result of parsing the playlist. + */ + virtual void onPlaylistParsed( + std::string playlistUrl, + std::queue urls, + PlaylistParseResult parseResult) = 0; +}; + +/** + * Write a @c PlaylistParseResult value to an @c ostream as a string. + * + * @param stream The stream to write the value to. + * @param result The PlaylistParseResult value to write to the @c ostream as a string. + * @return The @c ostream that was passed in and written to. + */ +inline std::ostream& operator<<(std::ostream& stream, const PlaylistParseResult& result) { + switch (result) { + case PlaylistParseResult::PARSE_RESULT_SUCCESS: + stream << "PARSE_RESULT_SUCCESS"; + break; + case PlaylistParseResult::PARSE_RESULT_UNHANDLED: + stream << "PARSE_RESULT_UNHANDLED"; + break; + case PlaylistParseResult::PARSE_RESULT_ERROR: + stream << "PARSE_RESULT_ERROR"; + break; + case PlaylistParseResult::PARSE_RESULT_IGNORED: + stream << "PARSE_RESULT_IGNORED"; + break; + case PlaylistParseResult::PARSE_RESULT_CANCELLED: + stream << "PARSE_RESULT_CANCELLED"; + break; + } + return stream; +} + +} // namespace playlistParser +} // namespace utils +} // namespace avsCommon +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AVS_COMMON_UTILS_INCLUDE_AVS_COMMON_UTILS_PLAYLIST_PARSER_PLAYLIST_PARSER_OBSERVER_INTERFACE_H_ diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/SDS/BufferLayout.h b/AVSCommon/Utils/include/AVSCommon/Utils/SDS/BufferLayout.h index ed57429bab..8d8d4696b1 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/SDS/BufferLayout.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/SDS/BufferLayout.h @@ -20,8 +20,9 @@ #include #include -#include #include +#include +#include #include "AVSCommon/Utils/Logger/LoggerUtils.h" #include "SharedDataStream.h" diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/Threading/TaskQueue.h b/AVSCommon/Utils/include/AVSCommon/Utils/Threading/TaskQueue.h index e4843be473..53c693d8dd 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/Threading/TaskQueue.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/Threading/TaskQueue.h @@ -43,24 +43,24 @@ class TaskQueue { TaskQueue(); /** - * Pushes a task on the back of the queue. If the queue is shutdown, the task will be a dropped, and an invalid + * Pushes a task on the back of the queue. If the queue is shutdown, the task will be dropped, and an invalid * future will be returned. * * @param task A task to push to the back of the queue. * @param args The arguments to call the task with. - * @returns A @c std::future to access the return value of the task. If the queue is shutdown, the task will be a + * @returns A @c std::future to access the return value of the task. If the queue is shutdown, the task will be * dropped, and an invalid future will be returned. */ template auto push(Task task, Args &&... args) -> std::future; /** - * Pushes a task on the front of the queue. If the queue is shutdown, the task will be a dropped, and an invalid + * Pushes a task on the front of the queue. If the queue is shutdown, the task will be dropped, and an invalid * future will be returned. * * @param task A task to push to the back of the queue. * @param args The arguments to call the task with. - * @returns A @c std::future to access the return value of the task. If the queue is shutdown, the task will be a + * @returns A @c std::future to access the return value of the task. If the queue is shutdown, the task will be * dropped, and an invalid future will be returned. */ template @@ -89,8 +89,24 @@ class TaskQueue { bool isShutdown(); private: + /// The queue type to use for holding tasks. + using Queue = std::deque>>; + + /** + * Pushes a task on the the queue. If the queue is shutdown, the task will be dropped, and an invalid + * future will be returned. + * + * @param front If @c true, push to the front of the queue, else push to the back. + * @param task A task to push to the front or back of the queue. + * @param args The arguments to call the task with. + * @returns A @c std::future to access the return value of the task. If the queue is shutdown, the task will be + * dropped, and an invalid future will be returned. + */ + template + auto pushTo(bool front, Task task, Args &&... args) -> std::future; + /// The queue of tasks - std::deque>> m_queue; + Queue m_queue; /// A condition variable to wait for new tasks to be placed on the queue. std::condition_variable m_queueChanged; @@ -104,36 +120,41 @@ class TaskQueue { template auto TaskQueue::push(Task task, Args &&... args) -> std::future { - // Remove arguments from the tasks type by binding the arguments to the task. - auto boundTask = std::bind(std::forward(task), std::forward(args)...); - - /* - * Create a std::packaged_task with the correct return type. The decltype only returns the return value of the - * boundTask. The following parentheses make it a function call with the boundTask return type. The package task - * will then return a future of the correct type. - */ - using PackagedTaskType = std::packaged_task; - auto packaged_task = std::make_shared(boundTask); + bool front = true; + return pushTo(!front, std::forward(task), std::forward(args)...); +} - // Remove the return type from the task by wrapping it in a lambda with no return value. - auto translated_task = [packaged_task]() { packaged_task->operator()(); }; +template +auto TaskQueue::pushToFront(Task task, Args &&... args) -> std::future { + bool front = true; + return pushTo(front, std::forward(task), std::forward(args)...); +} - { - std::lock_guard queueLock{m_queueMutex}; - if (!m_shutdown) { - m_queue.emplace_back(new std::function(translated_task)); - } else { - using FutureType = decltype(task(args...)); - return std::future(); - } - } +/** + * Utility function which waits for a @c std::future to be fulfilled and forward the result to a @c std::promise. + * + * @param promise The @c std::promise to fulfill when @c future is fulfilled. + * @param future The @c std::future on which to wait for a result to forward to @c promise. + */ +template +inline static void forwardPromise(std::shared_ptr> promise, std::future * future) { + promise->set_value(future->get()); +} - m_queueChanged.notify_all(); - return packaged_task->get_future(); +/** + * Specialization of @c forwardPromise() for @c void types. + * + * @param promise The @c std::promise to fulfill when @c future is fulfilled. + * @param future The @c std::future on which to wait before fulfilling @c promise. + */ +template <> +inline void forwardPromise(std::shared_ptr> promise, std::future * future) { + future->get(); + promise->set_value(); } template -auto TaskQueue::pushToFront(Task task, Args &&... args) -> std::future { +auto TaskQueue::pushTo(bool front, Task task, Args &&... args) -> std::future { // Remove arguments from the tasks type by binding the arguments to the task. auto boundTask = std::bind(std::forward(task), std::forward(args)...); @@ -141,17 +162,42 @@ auto TaskQueue::pushToFront(Task task, Args &&... args) -> std::future; auto packaged_task = std::make_shared(boundTask); + // Create a promise/future that we will fulfill when we have cleaned up the task. + auto cleanupPromise = std::make_shared>(); + auto cleanupFuture = cleanupPromise->get_future(); + // Remove the return type from the task by wrapping it in a lambda with no return value. - auto translated_task = [packaged_task]() { packaged_task->operator()(); }; + auto translated_task = [packaged_task, cleanupPromise]() mutable { + // Execute the task. + packaged_task->operator()(); + // Note the future for the task's result. + auto taskFuture = packaged_task->get_future(); + // Clean up the task. + packaged_task.reset(); + // Forward the task's result to our cleanup promise/future. + forwardPromise(cleanupPromise, &taskFuture); + }; + + // Release our local reference to packaged task so that the only remaining reference is inside the lambda. + packaged_task.reset(); { std::lock_guard queueLock{m_queueMutex}; if (!m_shutdown) { - m_queue.emplace_front(new std::function(translated_task)); + m_queue.emplace(front ? m_queue.begin() : m_queue.end(), new std::function(translated_task)); } else { using FutureType = decltype(task(args...)); return std::future(); @@ -159,7 +205,7 @@ auto TaskQueue::pushToFront(Task task, Args &&... args) -> std::futureget_future(); + return cleanupFuture; } } // namespace threading @@ -168,4 +214,3 @@ auto TaskQueue::pushToFront(Task task, Args &&... args) -> std::future + +#include "AVSCommon/Utils/Logger/LoggerSinkManager.h" + +namespace alexaClientSDK { +namespace avsCommon { +namespace utils { +namespace logger { + +LoggerSinkManager& LoggerSinkManager::instance() { + static LoggerSinkManager singleLoggerSinkManager; + return singleLoggerSinkManager; +} + +void LoggerSinkManager::addSinkObserver(SinkObserverInterface* observer) { + if (!observer) { + return; + } + + { + std::lock_guard lock(m_sinkObserverMutex); + m_sinkObservers.push_back(observer); + } + + // notify this observer of the current sink right away + observer->onSinkChanged(*m_sink); +} + +void LoggerSinkManager::removeSinkObserver(SinkObserverInterface* observer) { + if (!observer) { + return; + } + std::lock_guard lock(m_sinkObserverMutex); + m_sinkObservers.erase( + std::remove(m_sinkObservers.begin(), m_sinkObservers.end(), observer), + m_sinkObservers.end()); +} + +void LoggerSinkManager::changeSinkLogger(Logger& sink) { + if (m_sink == &sink) { + // don't do anything if the sink is the same + return; + } + + // copy the vector first with the lock + std::vector observersCopy; + { + std::lock_guard lock(m_sinkObserverMutex); + observersCopy = m_sinkObservers; + } + + m_sink = &sink; + + // call the callbacks + for (auto observer : observersCopy) { + observer->onSinkChanged(*m_sink); + } +} + +LoggerSinkManager::LoggerSinkManager() : + m_sink(&ACSDK_GET_SINK_LOGGER()) { +} + +} // namespace logger +} // namespace avsCommon +} // namespace utils +} // namespace alexaClientSDK diff --git a/AVSCommon/Utils/src/Logger/ModuleLogger.cpp b/AVSCommon/Utils/src/Logger/ModuleLogger.cpp index b459024823..c4d8c0ed99 100644 --- a/AVSCommon/Utils/src/Logger/ModuleLogger.cpp +++ b/AVSCommon/Utils/src/Logger/ModuleLogger.cpp @@ -17,6 +17,7 @@ // ModuleLogger.h will be indirectly pulled in through Logger.h when ACSDK_LOG_MODULE is defined. #include "AVSCommon/Utils/Logger/Logger.h" +#include "AVSCommon/Utils/Logger/LoggerSinkManager.h" namespace alexaClientSDK { namespace avsCommon { @@ -29,7 +30,7 @@ void ModuleLogger::emit( const char *threadId, const char *text) { if (shouldLog(level)) { - m_sink.emit(level, time, threadId, text); + m_sink.load()->emit(level, time, threadId, text); } } @@ -49,19 +50,32 @@ void ModuleLogger::onLogLevelChanged(Level level) { } } -ModuleLogger::ModuleLogger(const std::string& configKey, Logger& sink) : +void ModuleLogger::onSinkChanged(Logger& logger) { + if (m_sink.load()) { + m_sink.load()->removeLogLevelObserver(this); + } + m_sink = &logger; + m_sink.load()->addLogLevelObserver(this); +} + +ModuleLogger::ModuleLogger(const std::string& configKey) : Logger(Level::UNKNOWN), m_useSinkLogLevel(true), - m_sink(sink) { + m_sink(nullptr) { /* * Note that m_useSinkLogLevel is set to true by default. The idea is for * the ModuleLogger to use the same logLevel as its sink unless it's been * set specifically. - * - * By adding itself as an observer, the logLevel of the ModuleLogger will - * be set to be the same as the one in the sink. */ - m_sink.addLogLevelObserver(this); + + /* + * By adding itself to the LoggerSinkManager, the LoggerSinkManager will + * notify the ModuleLogger of the current sink logger via the + * SinkObserverInterface callback. And in the onSinkChanged callback, + * upon adding itself as a logLevel observer, the sink will notify the + * ModuleLogger the current logLevel via the onLogLevelChanged callback. + */ + LoggerSinkManager::instance().addSinkObserver(this); init(configuration::ConfigurationNode::getRoot()[configKey]); } diff --git a/AVSCommon/Utils/src/TimeUtils.cpp b/AVSCommon/Utils/src/TimeUtils.cpp index 5d74646ac0..a9ce7e4f5a 100644 --- a/AVSCommon/Utils/src/TimeUtils.cpp +++ b/AVSCommon/Utils/src/TimeUtils.cpp @@ -16,6 +16,8 @@ * limitations under the License. */ +#include + #include "AVSCommon/Utils/Timing/TimeUtils.h" #include "AVSCommon/Utils/Logger/Logger.h" @@ -99,8 +101,13 @@ bool convert8601TimeStringToUnix(const std::string & timeString, int64_t* unixTi return false; } - time_t rawtime = time(0); - tm* timeInfo = localtime(&rawtime); + std::time_t rawtime = std::time(0); + auto timeInfo = std::localtime(&rawtime); + + if (!timeInfo) { + ACSDK_ERROR(LX("convert8601TimeStringToUnixFailed").m("localtime returned nullptr.")); + return false; + } if (timeString.length() != ENCODED_TIME_STRING_EXPECTED_LENGTH) { ACSDK_ERROR(LX("convert8601TimeStringToUnixFailed").d("unexpected time string length:", timeString.length())); @@ -147,23 +154,32 @@ bool convert8601TimeStringToUnix(const std::string & timeString, int64_t* unixTi timeInfo->tm_year -= 1900; timeInfo->tm_mon -= 1; - *unixTime = static_cast(mktime(timeInfo)); + *unixTime = static_cast(std::mktime(timeInfo)); return true; } -int64_t getCurrentUnixTime() { +bool getCurrentUnixTime(int64_t* currentTime) { // TODO : ACSDK-387 to investigate a simpler and more portable implementation of this logic. - time_t rawtime = time(0); - tm* timeInfo = localtime(&rawtime); - int64_t unixEpochNow = static_cast(mktime(timeInfo)); + if (!currentTime) { + ACSDK_ERROR(LX("getCurrentUnixTimeFailed").m("currentTime parameter was nullptr.")); + return false; + } + + std::time_t rawtime = std::time(0); + auto timeInfo = std::localtime(&rawtime); + if (!timeInfo) { + ACSDK_ERROR(LX("getCurrentUnixTimeFailed").m("localtime returned nullptr.")); + return false; + } + *currentTime = static_cast(std::mktime(timeInfo)); - return unixEpochNow; + return true; } } // namespace timing } // namespace utils } // namespace avsCommon -} // namespace alexaClientSDK \ No newline at end of file +} // namespace alexaClientSDK diff --git a/AVSCommon/Utils/test/ExecutorTest.cpp b/AVSCommon/Utils/test/ExecutorTest.cpp index bfd44d7cf7..54d4e5d79f 100644 --- a/AVSCommon/Utils/test/ExecutorTest.cpp +++ b/AVSCommon/Utils/test/ExecutorTest.cpp @@ -15,6 +15,7 @@ * permissions and limitations under the License. */ +#include #include #include "ExecutorTestUtils.h" @@ -129,6 +130,91 @@ TEST_F(ExecutorTest, submitFunctionWithObjectReturnTypeObjectArgsAndVerifyExecut ASSERT_EQ(future.get().getValue(), value.getValue()); } +TEST_F(ExecutorTest, submitToFront) { + std::atomic ready(false); + std::atomic blocked(false); + std::list order; + + // submit a task which will block the executor + executor.submit( + [&] { + blocked = true; + while (!ready) { + std::this_thread::yield(); + } + } + ); + + // wait for it to block + while (!blocked) { + std::this_thread::yield(); + } + + // submit a task to the empty queue + executor.submit([&] { order.push_back(1); }); + + // submit a task to the back of the queue + executor.submit([&] { order.push_back(2); }); + + // submit a task to the front of the queue + executor.submitToFront([&] { order.push_back(3); }); + + // unblock the executor + ready = true; + + // wait for all tasks to complete + executor.waitForSubmittedTasks(); + + // verify execution order + ASSERT_EQ(order.size(), 3U); + ASSERT_EQ(order.front(), 3); + ASSERT_EQ(order.back(), 2); +} + +/// Used by @c futureWaitsForTaskCleanup delay and timestamp the time of lambda parameter destruction. +struct SlowDestructor { + /// Constructor. + SlowDestructor() : cleanedUp{nullptr} { + } + + /// Destructor which delays destruction, timestamps, and notifies a condition variable when it is done + ~SlowDestructor() { + if (cleanedUp) { + /* Delay briefly so that there is a measurable delay between the completion of the lambda's content and the + cleanup of the lambda's parameters */ + std::this_thread::sleep_for(SHORT_TIMEOUT_MS / 10); + + // Note the time when the destructor has (nominally) completed. + *cleanedUp = true; + } + } + + /** + * Boolean indicating destruction is completed (if != nullptr). Mutable so that a lambda can write to it in a + * SlowDestructor parameter that is captured by value. + */ + mutable std::atomic * cleanedUp; + +}; + +/// This test verifies that the executor waits to fulfill its promise until after the task is cleaned up. +TEST_F(ExecutorTest, futureWaitsForTaskCleanup) { + std::atomic cleanedUp(false); + SlowDestructor slowDestructor; + + // Submit a lambda to execute which captures a parameter by value that is slow to destruct. + executor.submit( + [slowDestructor, &cleanedUp] { + // Update the captured copy of slowDestructor so that it will delay destruction and update the cleanedUp + // flag. + slowDestructor.cleanedUp = &cleanedUp; + } + // wait for the promise to be fulfilled. + ).wait(); + + ASSERT_TRUE(cleanedUp); +} + } // namespace test } // namespace threading } // namespace avsCommon diff --git a/AVSCommon/Utils/test/LoggerTest.cpp b/AVSCommon/Utils/test/LoggerTest.cpp index 3f1b25bd1b..907734a4fa 100644 --- a/AVSCommon/Utils/test/LoggerTest.cpp +++ b/AVSCommon/Utils/test/LoggerTest.cpp @@ -15,14 +15,12 @@ * permissions and limitations under the License. */ -/// (for Logger.h) Name of type of Logger to send logs to use (send logs to @c getLoggerTestLogger(), defined below.) -#define ACSDK_LOG_SINK LoggerTest - #include #include #include #include "AVSCommon/Utils/Logger/Logger.h" +#include "AVSCommon/Utils/Logger/LoggerSinkManager.h" namespace alexaClientSDK { namespace avsCommon { @@ -71,6 +69,8 @@ static const std::string UNESCAPED_METADATA_VALUE = R"(reserved_chars['\' ',' ': /// String used to test that the message component is logged static const std::string TEST_MESSAGE_STRING = "Hello World!"; +/// Another String used to test that the message component is logged +static const std::string TEST_MESSAGE_STRING_1 = "World Hello!"; /** * Mock derivation of Logger for verifying calls and parameters to those calls. @@ -212,7 +212,8 @@ MockModuleLogger::MockModuleLogger() : ModuleLogger(ACSDK_STRINGIFY(ACSDK_LOG_SI } MockModuleLogger::~MockModuleLogger() { - m_sink.removeLogLevelObserver(this); + LoggerSinkManager::instance().removeSinkObserver(this); + m_sink.load()->removeLogLevelObserver(this); } /** @@ -220,6 +221,7 @@ MockModuleLogger::~MockModuleLogger() { */ class LoggerTest : public ::testing::Test { protected: + void SetUp() override; void TearDown() override; /** @@ -235,6 +237,11 @@ class LoggerTest : public ::testing::Test { void exerciseLevels(); }; +void LoggerTest::SetUp() { + // make sure getLoggerTestLogger() is used as sink + LoggerSinkManager::instance().changeSinkLogger(getLoggerTestLogger()); +} + void LoggerTest::TearDown() { g_log.reset(); } @@ -583,11 +590,11 @@ TEST_F(LoggerTest, testSensitiveDataSuppressed) { */ TEST_F(LoggerTest, testModuleLoggerObserver) { MockModuleLogger mockModuleLogger; - ACSDK_GET_SINK_LOGGER().setLevel(Level::WARN); + getLoggerTestLogger().setLevel(Level::WARN); ASSERT_EQ(mockModuleLogger.getLogLevel(), Level::WARN); mockModuleLogger.setLevel(Level::CRITICAL); ASSERT_EQ(mockModuleLogger.getLogLevel(), Level::CRITICAL); - ACSDK_GET_SINK_LOGGER().setLevel(Level::NONE); + getLoggerTestLogger().setLevel(Level::NONE); ASSERT_EQ(mockModuleLogger.getLogLevel(), Level::CRITICAL); } @@ -599,7 +606,7 @@ TEST_F(LoggerTest, testMultipleModuleLoggerObservers) { MockModuleLogger mockModuleLogger2; MockModuleLogger mockModuleLogger3; - ACSDK_GET_SINK_LOGGER().setLevel(Level::WARN); + getLoggerTestLogger().setLevel(Level::WARN); ASSERT_EQ(mockModuleLogger1.getLogLevel(), Level::WARN); ASSERT_EQ(mockModuleLogger2.getLogLevel(), Level::WARN); ASSERT_EQ(mockModuleLogger3.getLogLevel(), Level::WARN); @@ -609,12 +616,39 @@ TEST_F(LoggerTest, testMultipleModuleLoggerObservers) { ASSERT_EQ(mockModuleLogger2.getLogLevel(), Level::WARN); ASSERT_EQ(mockModuleLogger3.getLogLevel(), Level::WARN); - ACSDK_GET_SINK_LOGGER().setLevel(Level::NONE); + getLoggerTestLogger().setLevel(Level::NONE); ASSERT_EQ(mockModuleLogger1.getLogLevel(), Level::CRITICAL); ASSERT_EQ(mockModuleLogger2.getLogLevel(), Level::NONE); ASSERT_EQ(mockModuleLogger3.getLogLevel(), Level::NONE); } +/** + * Test changing of sink logger using the LoggerSinkManager. Expect the sink in + * ModuleLoggers will be changed. + */ +TEST_F(LoggerTest, testChangeSinkLogger) { + g_log = MockLogger::create(); + std::shared_ptr sink1 = MockLogger::create(); + + // reset loglevel to INFO + getLoggerTestLogger().setLevel(Level::INFO); + + // ModuleLoggers uses TestLogger as sink, so there shouldn't be any message + // sent to sink1 + ACSDK_INFO(LX(TEST_MESSAGE_STRING)); + ASSERT_NE(g_log->m_lastText.find(TEST_MESSAGE_STRING), std::string::npos); + ASSERT_EQ(sink1->m_lastText.find(TEST_MESSAGE_STRING), std::string::npos); + + // change to use sink1, now log message should be sent to sink1 + LoggerSinkManager::instance().changeSinkLogger(*sink1); + ACSDK_INFO(LX(TEST_MESSAGE_STRING_1)); + ASSERT_NE(g_log->m_lastText.find(TEST_MESSAGE_STRING), std::string::npos); + ASSERT_NE(sink1->m_lastText.find(TEST_MESSAGE_STRING_1), std::string::npos); + + // reset to the default sink to avoid messing up with subsequent tests + LoggerSinkManager::instance().changeSinkLogger(getLoggerTestLogger()); +} + } // namespace test } // namespace logger } // namespace avsCommon diff --git a/ApplicationUtilities/DefaultClient/include/DefaultClient/DefaultClient.h b/ApplicationUtilities/DefaultClient/include/DefaultClient/DefaultClient.h index 7680c4e6cc..b98c5d2422 100644 --- a/ApplicationUtilities/DefaultClient/include/DefaultClient/DefaultClient.h +++ b/ApplicationUtilities/DefaultClient/include/DefaultClient/DefaultClient.h @@ -25,11 +25,13 @@ #include #include #include +#include #include #include #include #include #include +#include namespace alexaClientSDK { namespace defaultClient { @@ -48,6 +50,7 @@ class DefaultClient { * connect() after creation. * * @param speakMediaPlayer The media player to use to play Alexa speech from. + * @param audioMediaPlayer The media player to use to play Alexa audio content from. * @param alertsMediaPlayer The media player to use to play alerts from. * @param authDelegate The component that provides the client with valid LWA authorization. * @Param alertStorage The storage interface that will be used to store alerts. @@ -62,6 +65,7 @@ class DefaultClient { */ static std::unique_ptr create( std::shared_ptr speakMediaPlayer, + std::shared_ptr audioMediaPlayer, std::shared_ptr alertsMediaPlayer, std::shared_ptr authDelegate, std::shared_ptr alertStorage, @@ -181,6 +185,7 @@ class DefaultClient { * Initializes the SDK and "glues" all the components together. * * @param speakMediaPlayer The media player to use to play Alexa speech from. + * @param audioMediaPlayer The media player to use to play Alexa audio content from. * @param alertsMediaPlayer The media player to use to play alerts from. * @param authDelegate The component that provides the client with valid LWA authorization. * @Param alertStorage The storage interface that will be used to store alerts. @@ -191,6 +196,7 @@ class DefaultClient { */ bool initialize( std::shared_ptr speakMediaPlayer, + std::shared_ptr audioMediaPlayer, std::shared_ptr alertsMediaPlayer, std::shared_ptr authDelegate, std::shared_ptr alertStorage, @@ -211,14 +217,20 @@ class DefaultClient { /// The audio input processor. std::shared_ptr m_audioInputProcessor; + /// The audio player. + std::shared_ptr m_audioPlayer; + /// The alerts capability agent. std::shared_ptr m_alertsCapabilityAgent; /// The Alexa dialog UX aggregator. std::shared_ptr m_dialogUXStateAggregator; + + /// The state synchronizer. + std::shared_ptr m_stateSynchronizer; }; } // namespace defaultClient } // namespace alexaClientSDK -#endif // ALEXA_CLIENT_SDK_DEFAULT_CLIENT_INCLUDE_DEFAULT_CLIENT_H_ \ No newline at end of file +#endif // ALEXA_CLIENT_SDK_DEFAULT_CLIENT_INCLUDE_DEFAULT_CLIENT_H_ diff --git a/ApplicationUtilities/DefaultClient/src/CMakeLists.txt b/ApplicationUtilities/DefaultClient/src/CMakeLists.txt index 5de4eca343..a4a2c029d1 100644 --- a/ApplicationUtilities/DefaultClient/src/CMakeLists.txt +++ b/ApplicationUtilities/DefaultClient/src/CMakeLists.txt @@ -12,6 +12,7 @@ target_link_libraries(DefaultClient AFML AIP Alerts - SpeechSynthesizer + SpeechSynthesizer + AudioPlayer AVSSystem - ContextManager) \ No newline at end of file + ContextManager) diff --git a/ApplicationUtilities/DefaultClient/src/DefaultClient.cpp b/ApplicationUtilities/DefaultClient/src/DefaultClient.cpp index 2cc75c261a..2c53ba252c 100644 --- a/ApplicationUtilities/DefaultClient/src/DefaultClient.cpp +++ b/ApplicationUtilities/DefaultClient/src/DefaultClient.cpp @@ -25,7 +25,8 @@ #include #include #include -#include +#include +#include namespace alexaClientSDK { namespace defaultClient { @@ -42,6 +43,7 @@ static const std::string TAG("DefaultClient"); std::unique_ptr DefaultClient::create( std::shared_ptr speakMediaPlayer, + std::shared_ptr audioMediaPlayer, std::shared_ptr alertsMediaPlayer, std::shared_ptr authDelegate, std::shared_ptr alertStorage, @@ -52,6 +54,7 @@ std::unique_ptr DefaultClient::create( std::unique_ptr defaultClient(new DefaultClient()); if (!defaultClient->initialize( speakMediaPlayer, + audioMediaPlayer, alertsMediaPlayer, authDelegate, alertStorage, @@ -65,6 +68,7 @@ std::unique_ptr DefaultClient::create( bool DefaultClient::initialize( std::shared_ptr speakMediaPlayer, + std::shared_ptr audioMediaPlayer, std::shared_ptr alertsMediaPlayer, std::shared_ptr authDelegate, std::shared_ptr alertStorage, @@ -77,6 +81,11 @@ bool DefaultClient::initialize( return false; } + if (!audioMediaPlayer) { + ACSDK_ERROR(LX("initializeFailed").d("reason", "nullAudioMediaPlayer")); + return false; + } + if (!alertsMediaPlayer) { ACSDK_ERROR(LX("initializeFailed").d("reason", "nullAlertsMediaPlayer")); return false; @@ -178,14 +187,27 @@ bool DefaultClient::initialize( * Creating the state synchronizer - This component is responsible for updating AVS of the state of all components * whenever a new connection is established as part of the System interface of AVS. */ - auto stateSynchronizer = capabilityAgents::system::StateSynchronizer::create( - contextManager, m_connectionManager); - if (!stateSynchronizer) { + // TODO: ACSDK-421: Revert this to use m_connectionManager rather than messageRouter. + m_stateSynchronizer = capabilityAgents::system::StateSynchronizer::create( + contextManager, messageRouter); + if (!m_stateSynchronizer) { ACSDK_ERROR(LX("initializeFailed").d("reason", "unableToCreateStateSynchronizer")); return false; } - m_connectionManager->addConnectionStatusObserver(stateSynchronizer); + m_connectionManager->addConnectionStatusObserver(m_stateSynchronizer); + m_stateSynchronizer->addObserver(m_connectionManager); + + /* + * Creating the User Inactivity Monitor - This component is responsibly for updating AVS of user inactivity as + * described in the System Interface of AVS. + */ + auto userInactivityMonitor = capabilityAgents::system::UserInactivityMonitor::create( + m_connectionManager, exceptionSender); + if (!userInactivityMonitor) { + ACSDK_ERROR(LX("initializeFailed").d("reason", "unableToCreateUserInactivityMonitor")); + return false; + } /* * Creating the Audio Input Processor - This component is the Capability Agent that implments the SpeechRecognizer @@ -197,7 +219,8 @@ bool DefaultClient::initialize( contextManager, m_focusManager, m_dialogUXStateAggregator, - exceptionSender); + exceptionSender, + userInactivityMonitor); if (!m_audioInputProcessor) { ACSDK_ERROR(LX("initializeFailed").d("reason", "unableToCreateAudioInputProcessor")); return false; @@ -224,6 +247,22 @@ bool DefaultClient::initialize( speechSynthesizer->addObserver(m_dialogUXStateAggregator); + /* + * Creating the Audio Player - This component is the Capability Agent that implements the AudioPlayer + * interface of AVS. + */ + m_audioPlayer = capabilityAgents::audioPlayer::AudioPlayer::create( + audioMediaPlayer, + m_connectionManager, + m_focusManager, + contextManager, + attachmentManager, + exceptionSender); + if (!m_audioPlayer) { + ACSDK_ERROR(LX("initializeFailed").d("reason", "unableToCreateAudioPlayer")); + return false; + } + /* * Creating the Alerts Capability Agent - This component is the Capability Agent that implements the Alerts * interface of AVS. @@ -243,6 +282,16 @@ bool DefaultClient::initialize( m_connectionManager->addConnectionStatusObserver(m_alertsCapabilityAgent); + /* + * Creating the Endpoint Handler - This component is responsible for handling directives from AVS instructing the + * client to change the endpoint to connect to. + */ + auto endpointHandler = capabilityAgents::system::EndpointHandler::create(m_connectionManager, exceptionSender); + if (!endpointHandler) { + ACSDK_ERROR(LX("initializeFailed").d("reason", "unableToCreateEndpointHandler")); + return false; + } + /* * The following two statements show how to register capability agents to the directive sequencer. */ @@ -253,6 +302,13 @@ bool DefaultClient::initialize( return false; } + if (!m_directiveSequencer->addDirectiveHandler(m_audioPlayer)) { + ACSDK_ERROR(LX("initializeFailed") + .d("reason", "unableToRegisterDirectiveHandler") + .d("directiveHandler", "AudioPlayer")); + return false; + } + if (!m_directiveSequencer->addDirectiveHandler(m_audioInputProcessor)) { ACSDK_ERROR(LX("initializeFailed") .d("reason", "unableToRegisterDirectiveHandler") @@ -267,6 +323,20 @@ bool DefaultClient::initialize( return false; } + if (!m_directiveSequencer->addDirectiveHandler(endpointHandler)) { + ACSDK_ERROR(LX("initializeFailed") + .d("reason", "unableToRegisterDirectiveHandler") + .d("directiveHandler", "EndpointHandler")); + return false; + } + + if (!m_directiveSequencer->addDirectiveHandler(userInactivityMonitor)) { + ACSDK_ERROR(LX("initializeFailed") + .d("reason", "unableToRegisterDirectiveHandler") + .d("directiveHandler", "UserInactivityMonitor")); + return false; + } + return true; } @@ -338,11 +408,17 @@ DefaultClient::~DefaultClient() { if (m_directiveSequencer) { m_directiveSequencer->shutdown(); } + if (m_stateSynchronizer) { + m_stateSynchronizer->shutdown(); + } if (m_audioInputProcessor) { m_audioInputProcessor->resetState().get(); } + if (m_audioPlayer) { + m_audioPlayer->shutdown(); + } m_audioInputProcessor->removeObserver(m_dialogUXStateAggregator); } } // namespace defaultClient -} // namespace alexaClientSDK \ No newline at end of file +} // namespace alexaClientSDK diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..9d2a966c10 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,162 @@ +## ChangeLog + +### [1.0.0] - 2017-08-07 + +* Added `AudioPlayer` capability agent. + * Supports iHeartRadio. +* `StateSynchronizer` has been updated to better enforce that `System.SynchronizeState` is the first Event sent on a connection to AVS. +* Additional tests have been added to `ACL`. +* The `Sample App` has been updated with several small fixes and improvements. +* `ADSL` was updated such that all directives are now blocked while the handling of previous `SpeechSynthesizer.Speak` directives complete. Because any directive may now be blocked, the `preHandleDirective() / handleDirective()` path is now used for handling all directives. +* Fixes for the following GitHub issues: + * [EXPECTING_SPEECH + SPEAK directive simultaneously on multi-turn conversation](https://github.com/alexa/alexa-client-sdk/issues/44). +* A bug causing `ACL` to not send a ping to AVS every 5 minutes, leading to periodic server disconnects, was fixed. +* Subtle race condition issues were addressed in the `Executor` class, resolving some intermittent crashes. +* Known Issues + * Native components for the following capability agents are **not** included in this release: `PlaybackController`, `Speaker`, `Settings`, `TemplateRuntime`, and `Notifications`. + * `MediaPlayer` + * Long periods of buffer underrun can cause an error related with seeking and subsequent stopped playback. + * Long periods of buffer underrun can cause flip flopping between buffer_underrun and playing states. + * Playlist parsing is not supported unless -DTOTEM_PLPARSER=ON is specified. + * `AudioPlayer` + * Amazon Music, TuneIn, and SiriusXM are not supported in this release. + * Our parsing of urls currently depends upon GNOME/totem-pl-parser which only works on some Linux platforms. + * `AlertsCapabilityAgent` + * Satisfies the [AVS specification](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/timers-and-alarms-conceptual-overview) except for sending retrospective Events. For example, sending `AlertStarted` Event for an Alert which rendered when there was no internet connection. + * `Sample App`: + * Any connection loss during the `Listening` state keeps the app stuck in that state, unless the ongoing interaction is manually stopped by the user. + * The user must wait several seconds after starting up the sample app before the sample app is properly usable. + * `Tests`: + * `SpeechSynthesizer` unit tests hang on some older versions of GCC due to a tear down issue in the test suite + * Intermittent Alerts integration test failures caused by rigidness in expected behavior in the tests + +### [0.6.0] - 2017-07-14 + +* Added a sample app that leverages the SDK. +* Added `Alerts` capability agent. +* Added the `DefaultClient` class. +* Added the following classes to support directives and events in the [`System` interface](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/system): `StateSynchronizer`, `EndpointHandler`, and `ExceptionEncounteredSender`. +* Added unit tests for `ACL`. +* Updated `MediaPlayer` to play local files given an `std::istream`. +* Changed build configuration from `Debug` to `Release`. +* Removed `DeprecatedLogger` class. +* Known Issues: + * `MediaPlayer`: Our `GStreamer` based implementation of `MediaPlayer` is not fully robust, and may result in fatal runtime errors, under the following conditions: + * Attempting to play multiple simultaneous audio streams + * Calling `MediaPlayer::play()` and `MediaPlayer::stop()` when the MediaPlayer is already playing or stopped, respectively. + * Other miscellaneous issues, which will be addressed in the near future + * `AlertsCapabilityAgent`: + * This component has been temporarily simplified to work around the known `MediaPlayer` issues mentioned above + * Fully satisfies the AVS specification except for sending retrospective Events, for example, sending `AlertStarted` for an Alert which rendered when there was no Internet connection + * This component is not fully thread-safe, however, this will be addressed shortly + * Alerts currently run indefinitely until stopped manually by the user. This will be addressed shortly by having a timeout value for an alert to stop playing. + * Alerts do not play in the background when Alexa is speaking, but will continue playing after Alexa stops speaking. + * `Sample App`: + * Without the refresh token being filled out in the JSON file, the sample app crashes on start up. + * Any connection loss during the `Listening` state keeps the app stuck in that state, unless the ongoing interaction is manually stopped by the user. + * At the end of a shopping list with more than 5 items, the interaction in which Alexa asks the user if he/she would like to hear more does not finish properly. + * `Tests`: + * `SpeechSynthesizer` unit tests hang on some older versions of GCC due to a tear down issue in the test suite + * Intermittent Alerts integration test failures caused by rigidness in expected behavior in the tests + +### [0.5.0] - 2017-06-23 + +* Updated most SDK components to use new logging abstraction. +* Added a `getConfiguration()` method to `DirectiveHandlerInterface` to register capability agents with Directive Sequencer. +* Added `ACL` stream processing with pause and redrive. +* Removed the dependency of `ACL` library on `Authdelegate`. +* Added an interface to allow `ACL` to add/remove `ConnectionStatusObserverInterface`. +* Fixed compile errors in KITT.ai, `DirectiveHandler` and compiler warnings in `AIP` tests. +* Corrected formatting of code in many files. +* Fixes for the following GitHub issues: + * [MessageRequest callbacks never triggered if disconnected](https://github.com/alexa/alexa-client-sdk/issues/21) + * [AttachmentReader::read() returns ReadStatus::CLOSED if an AttachmentWriter has not been created yet](https://github.com/alexa/alexa-client-sdk/issues/25) + +### [0.4.1] - 2017-06-09 + +* Implemented Sensory wake word detector functionality. +* Removed the need for a `std::recursive_mutex` in `MessageRouter`. +* Added `AIP` unit tests. +* Added `handleDirectiveImmediately` functionality to `SpeechSynthesizer`. +* Added memory profiles for: + * AIP + * SpeechSynthesizer + * ContextManager + * AVSUtils + * AVSCommon +* Bug fix for `MessageRouterTest` aborting intermittently. +* Bug fix for `MultipartParser.h` compiler warning. +* Suppression of sensitive log data even in debug builds. Use CMake parameter -DACSDK_EMIT_SENSITIVE_LOGS=ON to allow logging of sensitive information in DEBUG builds. +* Fixed crash in `ACL` when attempting to use more than 10 streams. +* Updated `MediaPlayer` to use `autoaudiosink` instead of requiring `pulseaudio`. +* Updated `MediaPlayer` build to suppport local builds of GStreamer. +* Fixes for the following GitHub issues: + * [MessageRouter::send() does not take the m_connectionMutex](https://github.com/alexa/alexa-client-sdk/issues/5) + * [MessageRouter::disconnectAllTransportsLocked flow leads to erase while iterating transports vector](https://github.com/alexa/alexa-client-sdk/issues/8) + * [Build errors when building with KittAi enabled](https://github.com/alexa/alexa-client-sdk/issues/9) + * [HTTP2Transport race may lead to deadlock](https://github.com/alexa/alexa-client-sdk/issues/10) + * [Crash in HTTP2Transport::cleanupFinishedStreams()](https://github.com/alexa/alexa-client-sdk/issues/17) + * [The attachment writer interface should take a `const void*` instead of `void*`](https://github.com/alexa/alexa-client-sdk/issues/24) + +### [0.4.0] - 2017-05-31 (patch) + +* Added `AuthServer`, an authorization server implementation used to retrieve refresh tokens from LWA. + +### [0.4.0] - 2017-05-24 + +* Added `SpeechSynthesizer`, an implementation of the `SpeechRecognizer` capability agent. +* Implemented a reference `MediaPlayer` based on [GStreamer](https://gstreamer.freedesktop.org/) for audio playback. +* Added `MediaPlayerInterface` that allows you to implement your own media player. +* Updated `ACL` to support asynchronous receipt of audio attachments from AVS. +* Bug Fixes: + * Some intermittent unit test failures were fixed. +* Known Issues: + * `ACL`'s asynchronous receipt of audio attachments may manage resources poorly in scenarios where attachments are received but not consumed. + * When an `AttachmentReader` does not deliver data for prolonged periods `MediaPlayer` may not resume playing the delayed audio. + +### [0.3.0] - 2017-05-17 + +* Added the `CapabilityAgent` base class that is used to build capability agent implementations. +* Added `ContextManager`, a component that allows multiple capability agents to store and access state. These Events include `Context`, which is used to communicate the state of each capability agent to AVS in the following Events: + * [`Recognize`](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/speechrecognizer#recognize) + * [`PlayCommandIssued`](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/playbackcontroller#playcommandissued) + * [`PauseCommandIssued`](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/playbackcontroller#pausecommandissued) + * [`NextCommandIssued`](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/playbackcontroller#nextcommandissued) + * [`PreviousCommandIssued`](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/playbackcontroller#previouscommandissued) + * [`SynchronizeState`](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/system#synchronizestate) + * [`ExceptionEncountered`](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/system#exceptionencountered) +* Added `SharedDataStream` (SDS) to asynchronously communicate data between a local reader and writer. +* Added `AudioInputProcessor` (AIP), an implementation of a `SpeechRecognizer` capability agent. +* Added WakeWord Detector (WWD), which recognizes keywords in audio streams. [0.3.0] implements a wrapper for KITT.ai. +* Added a new implementation of `AttachmentManager` and associated classes for use with SDS. +* Updated `ACL` to support asynchronously sending audio to AVS. + +### [0.2.1] - 2017-05-03 + +* Replaced the configuration file `AuthDelegate.config` with `AlexaClientSDKConfig.json`. +* Added the ability to specify a `CURLOPT_CAPATH` value to be used when libcurl is used by ACL and AuthDelegate. See See Appendix C in the README for details. +* Changes to ADSL interfaces: + * The [0.2.0] interface for registering directive handlers (`DirectiveSequencer::setDirectiveHandlers()`) was problematic because it canceled the ongoing processing of directives and dropped further directives until it completed. The revised API makes the operation immediate without canceling or dropping any handling. However, it does create the possibility that `DirectiveHandlerInterface` methods `preHandleDirective()` and `handleDirective()` may be called on different handlers for the same directive. + * `DirectiveSequencerInterface::setDirectiveHandlers()` was replaced by `addDirectiveHandlers()` and `removeDirectiveHandlers()`. + * `DirectiveHandlerInterface::shutdown()` was replaced with `onDeregistered()`. + * `DirectiveHandlerInterface::preHandleDirective()` now takes a `std::unique_ptr` instead of a `std::shared_ptr` to `DirectiveHandlerResultInterface`. + * `DirectiveHandlerInterface::handleDirective()` now returns a bool indicating if the handler recognizes the `messageId`. +* Bug fixes: + * ACL and AuthDelegate now require TLSv1.2. + * `onDirective()` now sends `ExceptionEncountered` for unhandled directives. + * `DirectiveSequencer::shutdown()` no longer sends `ExceptionEncountered()` for queued directives. + +### [0.2.0] - 2017-03-27 (patch) + +* Added memory profiling for ACL and ADSL. See Appendix A in the README. +* Added a command to build the API documentation. + +### [0.2.0] - 2017-03-09 + +* Added `Alexa Directive Sequencer Library` (ADSL) and `Alexa Focus Manager Library` (AMFL). +* CMake build types and options have been updated. +* Documentation for libcurl optimization included. + +### [0.1.0] - 2017-02-10 + +* Initial release of the `Alexa Communications Library` (ACL), a component which manages network connectivity with AVS, and `AuthDelegate`, a component which handles user authorization with AVS. \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index b2db076fb5..a9fe5f592e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.1 FATAL_ERROR) # Set project information -project(AlexaClientSDK VERSION 0.6.0 LANGUAGES CXX) +project(AlexaClientSDK VERSION 0.0.0 LANGUAGES CXX) set(PROJECT_BRIEF "A cross-platform, modular SDK for interacting with the Alexa Voice Service") include(build/BuildDefaults.cmake) @@ -16,6 +16,7 @@ add_subdirectory("ADSL") add_subdirectory("AFML") add_subdirectory("ContextManager") add_subdirectory("MediaPlayer") +add_subdirectory("PlaylistParser") add_subdirectory("KWD") add_subdirectory("CapabilityAgents") add_subdirectory("Integration") diff --git a/CapabilityAgents/AIP/include/AIP/AudioInputProcessor.h b/CapabilityAgents/AIP/include/AIP/AudioInputProcessor.h index 495b3046e3..2c88ccdfae 100644 --- a/CapabilityAgents/AIP/include/AIP/AudioInputProcessor.h +++ b/CapabilityAgents/AIP/include/AIP/AudioInputProcessor.h @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -77,6 +78,7 @@ class AudioInputProcessor : * @param focusManager The channel focus manager used to manage usage of the dialog channel. * @param dialogUXStateAggregator The dialog state aggregator which tracks UX states related to dialog. * @param exceptionEncounteredSender The object to use for sending AVS Exception messages. + * @param userActivityNotifier The object to use for resetting user inactivity. * @param defaultAudioProvider A default @c avsCommon::AudioProvider to use for ExpectSpeech if the previous * provider is not readable (@c avsCommon::AudioProvider::alwaysReadable). This parameter is optional and * defaults to an invalid @c avsCommon::AudioProvider. @@ -89,6 +91,7 @@ class AudioInputProcessor : std::shared_ptr focusManager, std::shared_ptr dialogUXStateAggregator, std::shared_ptr exceptionEncounteredSender, + std::shared_ptr userActivityNotifier, AudioProvider defaultAudioProvider = AudioProvider::null()); /** @@ -215,6 +218,7 @@ class AudioInputProcessor : * @param contextManager The AVS Context manager used to generate system context for events. * @param focusManager The channel focus manager used to manage usage of the dialog channel. * @param exceptionEncounteredSender The object to use for sending ExceptionEncountered messages. + * @param userActivityNotifier The object to use for resetting user inactivity. * @param defaultAudioProvider A default @c avsCommon::AudioProvider to use for ExpectSpeech if the previous * provider is not readable (@c AudioProvider::alwaysReadable). This parameter is optional, and ignored if set * to @c AudioProvider::null(). @@ -229,6 +233,7 @@ class AudioInputProcessor : std::shared_ptr contextManager, std::shared_ptr focusManager, std::shared_ptr exceptionEncounteredSender, + std::shared_ptr userActivityNotifier, AudioProvider defaultAudioProvider); /** @@ -448,6 +453,9 @@ class AudioInputProcessor : /// The @c FocusManager used to manage usage of the dialog channel. std::shared_ptr m_focusManager; + /// The @c UserInactivityMonitor used to reset the inactivity timer of the user. + std::shared_ptr m_userActivityNotifier; + /// Timer which runs in the @c EXPECTING_SPEECH state. avsCommon::utils::timing::Timer m_expectingSpeechTimer; diff --git a/CapabilityAgents/AIP/src/AudioInputProcessor.cpp b/CapabilityAgents/AIP/src/AudioInputProcessor.cpp index cf0749f0e8..5f4356de69 100644 --- a/CapabilityAgents/AIP/src/AudioInputProcessor.cpp +++ b/CapabilityAgents/AIP/src/AudioInputProcessor.cpp @@ -66,6 +66,7 @@ std::shared_ptr AudioInputProcessor::create( std::shared_ptr focusManager, std::shared_ptr dialogUXStateAggregator, std::shared_ptr exceptionEncounteredSender, + std::shared_ptr userActivityNotifier, AudioProvider defaultAudioProvider) { if (!directiveSequencer) { ACSDK_ERROR(LX("createFailed").d("reason", "nullDirectiveSequencer")); @@ -85,6 +86,9 @@ std::shared_ptr AudioInputProcessor::create( } else if (!exceptionEncounteredSender) { ACSDK_ERROR(LX("createFailed").d("reason", "nullExceptionEncounteredSender")); return nullptr; + } else if (!userActivityNotifier) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullUserActivityNotifier")); + return nullptr; } auto aip = std::shared_ptr(new AudioInputProcessor( @@ -93,10 +97,13 @@ std::shared_ptr AudioInputProcessor::create( contextManager, focusManager, exceptionEncounteredSender, + userActivityNotifier, defaultAudioProvider)); - contextManager->setStateProvider(RECOGNIZER_STATE, aip); - dialogUXStateAggregator->addObserver(aip); + if (aip) { + contextManager->setStateProvider(RECOGNIZER_STATE, aip); + dialogUXStateAggregator->addObserver(aip); + } return aip; } @@ -258,12 +265,14 @@ AudioInputProcessor::AudioInputProcessor( std::shared_ptr contextManager, std::shared_ptr focusManager, std::shared_ptr exceptionEncounteredSender, + std::shared_ptr userActivityNotifier, AudioProvider defaultAudioProvider) : CapabilityAgent{NAMESPACE, exceptionEncounteredSender}, m_directiveSequencer{directiveSequencer}, m_messageSender{messageSender}, m_contextManager{contextManager}, m_focusManager{focusManager}, + m_userActivityNotifier{userActivityNotifier}, m_defaultAudioProvider{defaultAudioProvider}, m_lastAudioProvider{AudioProvider::null()}, m_state{ObserverInterface::State::IDLE}, @@ -589,7 +598,6 @@ bool AudioInputProcessor::executeStopCapture(bool stopImmediately, std::shared_p .d("state", m_state)); return false; } - // Create a lambda to do the StopCapture. std::function stopCapture = [=] { ACSDK_DEBUG(LX("stopCapture").d("stopImmediately", stopImmediately)); @@ -731,6 +739,12 @@ void AudioInputProcessor::setState(ObserverInterface::State state) { if (m_state == state) { return; } + + // Reset the user inactivity if transitioning to or from `RECOGNIZING` state. + if (ObserverInterface::State::RECOGNIZING == m_state || ObserverInterface::State::RECOGNIZING == state) { + m_userActivityNotifier->onUserActive(); + } + ACSDK_DEBUG(LX("setState").d("from", m_state).d("to", state)); m_state = state; for (auto observer: m_observers) { diff --git a/CapabilityAgents/AIP/test/AudioInputProcessorTest.cpp b/CapabilityAgents/AIP/test/AudioInputProcessorTest.cpp index 85dd6a5b12..221f3f264a 100644 --- a/CapabilityAgents/AIP/test/AudioInputProcessorTest.cpp +++ b/CapabilityAgents/AIP/test/AudioInputProcessorTest.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -614,6 +615,9 @@ class AudioInputProcessorTest: public ::testing::Test { /// The mock @c ExceptionEncounteredSenderInterface. std::shared_ptr m_mockExceptionEncounteredSender; + /// The mock @c UserActivityNotifierInterface. + std::shared_ptr m_mockUserActivityNotifier; + /// A @c AudioInputStream::Writer to write audio data to m_audioProvider. std::unique_ptr m_writer; @@ -645,6 +649,7 @@ void AudioInputProcessorTest::SetUp() { m_mockExceptionEncounteredSender = std::make_shared(); + m_mockUserActivityNotifier = std::make_shared(); size_t bufferSize = avsCommon::avs::AudioInputStream::calculateBufferSize(SDS_WORDS, SDS_WORDSIZE, SDS_MAXREADERS); auto buffer = std::make_shared(bufferSize); @@ -674,8 +679,9 @@ void AudioInputProcessorTest::SetUp() { m_mockFocusManager, m_dialogUXStateAggregator, m_mockExceptionEncounteredSender, + m_mockUserActivityNotifier, *m_audioProvider); - EXPECT_NE(m_audioInputProcessor, nullptr); + ASSERT_NE(m_audioInputProcessor, nullptr); m_audioInputProcessor->addObserver(m_dialogUXStateAggregator); // Note: StrictMock here so that we fail on unexpected AIP state changes m_mockObserver = std::make_shared>(); @@ -770,6 +776,7 @@ bool AudioInputProcessorTest::testRecognizeSucceeds( } if (!bargeIn) { + EXPECT_CALL(*m_mockUserActivityNotifier, onUserActive()).Times(2); EXPECT_CALL(*m_mockObserver, onStateChanged(AudioInputProcessorObserverInterface::State::RECOGNIZING)); EXPECT_CALL(*m_mockFocusManager, acquireChannel(CHANNEL_NAME, _, ACTIVITY_ID)) .WillOnce(InvokeWithoutArgs([this, stopPoint] { @@ -857,6 +864,7 @@ bool AudioInputProcessorTest::testContextFailure(avsCommon::sdkInterfaces::Conte EXPECT_CALL(*m_mockContextManager, getContext(_)) .WillOnce(InvokeWithoutArgs([this, error] { m_audioInputProcessor->onContextFailure(error); })); EXPECT_CALL(*m_mockObserver, onStateChanged(AudioInputProcessorObserverInterface::State::RECOGNIZING)); + EXPECT_CALL(*m_mockUserActivityNotifier, onUserActive()).Times(2); EXPECT_CALL(*m_mockObserver, onStateChanged(AudioInputProcessorObserverInterface::State::IDLE)) .WillOnce(InvokeWithoutArgs([&] { std::lock_guard lock(mutex); @@ -938,6 +946,7 @@ bool AudioInputProcessorTest::testExpectSpeechSucceeds(bool withDialogRequestId) EXPECT_CALL(*m_mockObserver, onStateChanged(AudioInputProcessorObserverInterface::State::EXPECTING_SPEECH)); EXPECT_CALL(*m_mockObserver, onStateChanged(AudioInputProcessorObserverInterface::State::RECOGNIZING)); + EXPECT_CALL(*m_mockUserActivityNotifier, onUserActive()).Times(2); if (withDialogRequestId) { EXPECT_CALL(*result, setCompleted()); } @@ -1113,7 +1122,8 @@ void AudioInputProcessorTest::removeDefaultAudioProvider() { m_mockContextManager, m_mockFocusManager, m_dialogUXStateAggregator, - m_mockExceptionEncounteredSender); + m_mockExceptionEncounteredSender, + m_mockUserActivityNotifier); EXPECT_NE(m_audioInputProcessor, nullptr); m_audioInputProcessor->addObserver(m_mockObserver); m_audioInputProcessor->addObserver(m_dialogUXStateAggregator); @@ -1130,6 +1140,7 @@ void AudioInputProcessorTest::makeDefaultAudioProviderNotAlwaysReadable() { m_mockFocusManager, m_dialogUXStateAggregator, m_mockExceptionEncounteredSender, + m_mockUserActivityNotifier, *m_audioProvider); EXPECT_NE(m_audioInputProcessor, nullptr); m_audioInputProcessor->addObserver(m_mockObserver); @@ -1170,6 +1181,7 @@ TEST_F(AudioInputProcessorTest, createWithoutDirectiveSequencer) { m_mockFocusManager, m_dialogUXStateAggregator, m_mockExceptionEncounteredSender, + m_mockUserActivityNotifier, *m_audioProvider); EXPECT_EQ(m_audioInputProcessor, nullptr); } @@ -1184,6 +1196,7 @@ TEST_F(AudioInputProcessorTest, createWithoutMessageSender) { m_mockFocusManager, m_dialogUXStateAggregator, m_mockExceptionEncounteredSender, + m_mockUserActivityNotifier, *m_audioProvider); EXPECT_EQ(m_audioInputProcessor, nullptr); } @@ -1198,6 +1211,7 @@ TEST_F(AudioInputProcessorTest, createWithoutContextManager) { m_mockFocusManager, m_dialogUXStateAggregator, m_mockExceptionEncounteredSender, + m_mockUserActivityNotifier, *m_audioProvider); EXPECT_EQ(m_audioInputProcessor, nullptr); } @@ -1212,6 +1226,7 @@ TEST_F(AudioInputProcessorTest, createWithoutFocusManager) { nullptr, m_dialogUXStateAggregator, m_mockExceptionEncounteredSender, + m_mockUserActivityNotifier, *m_audioProvider); EXPECT_EQ(m_audioInputProcessor, nullptr); } @@ -1226,6 +1241,7 @@ TEST_F(AudioInputProcessorTest, createWithoutStateAggregator) { m_mockFocusManager, nullptr, m_mockExceptionEncounteredSender, + m_mockUserActivityNotifier, *m_audioProvider); EXPECT_EQ(m_audioInputProcessor, nullptr); } @@ -1243,6 +1259,25 @@ TEST_F(AudioInputProcessorTest, createWithoutExceptionSender) { m_mockFocusManager, m_dialogUXStateAggregator, nullptr, + m_mockUserActivityNotifier, + *m_audioProvider); + EXPECT_EQ(m_audioInputProcessor, nullptr); +} + +/** + * Function to verify that @c AudioInputProcessor::create() errors out with an invalid + * @c UserActivityNotifierInterface. + */ +TEST_F(AudioInputProcessorTest, createWithoutUserActivityNotifier) { + m_audioInputProcessor->removeObserver(m_dialogUXStateAggregator); + m_audioInputProcessor = AudioInputProcessor::create( + m_mockDirectiveSequencer, + m_mockMessageSender, + m_mockContextManager, + m_mockFocusManager, + m_dialogUXStateAggregator, + m_mockExceptionEncounteredSender, + nullptr, *m_audioProvider); EXPECT_EQ(m_audioInputProcessor, nullptr); } @@ -1258,6 +1293,7 @@ TEST_F(AudioInputProcessorTest, createWithoutAudioProvider) { m_mockFocusManager, m_dialogUXStateAggregator, m_mockExceptionEncounteredSender, + m_mockUserActivityNotifier, AudioProvider::null()); EXPECT_NE(m_audioInputProcessor, nullptr); } diff --git a/CapabilityAgents/Alerts/include/Alerts/Alert.h b/CapabilityAgents/Alerts/include/Alerts/Alert.h index cb2e23c808..a906e433de 100644 --- a/CapabilityAgents/Alerts/include/Alerts/Alert.h +++ b/CapabilityAgents/Alerts/include/Alerts/Alert.h @@ -23,6 +23,7 @@ #include "Alerts/Renderer/RendererObserverInterface.h" #include +#include #include #include @@ -279,22 +280,6 @@ class Alert : public renderer::RendererObserverInterface { */ void snooze(const std::string & updatedScheduledTime_ISO_8601); - /** - * A temporary function to control if simple mode is enabled. - * Simple mode is a workaround for our gstreamer implementation behaving incorrectly. - * - * @return If simple mode is enabled. - */ - static bool isSimpleModeEnabled(); - - /** - * A temporary function to activate an alert in a simple manner. In particular, the caller will not receive - * any callbacks with respect to alert status changes. - * - * @param focusState The current focus state the alert should have. - */ - void activateSimple(avsCommon::avs::FocusState focusState); - private: /// A friend relationship, since our storage interface needs access to all fields. friend class storage::SQLiteAlertStorage; @@ -305,14 +290,9 @@ class Alert : public renderer::RendererObserverInterface { void startRenderer(); /** - * A utility function to start an alert's renderer in a simple mode, where the caller will not receive callbacks. - */ - void startRendererSimple(); - - /** - * A utility function to start an alert's renderer in a simple mode, where the caller will not receive callbacks. + * A utility function to be invoked when the maximum time for an alert has expired. */ - void stopRendererSimple(); + void onMaxTimerExpiration(); /// The AVS token for the alert. std::string m_token; @@ -337,6 +317,10 @@ class Alert : public renderer::RendererObserverInterface { std::shared_ptr m_renderer; /// The observer of the alert. AlertObserverInterface* m_observer; + /// A flag to capture if the maximum time timer has expired for this alert. + bool m_hasTimerExpired; + /// The timer to ensure this alert is not active longer than a maximum length of time. + avsCommon::utils::timing::Timer m_maxLengthTimer; }; } // namespace alerts diff --git a/CapabilityAgents/Alerts/include/Alerts/Renderer/Renderer.h b/CapabilityAgents/Alerts/include/Alerts/Renderer/Renderer.h index 0e0a9a79e2..8b3be5c0fe 100644 --- a/CapabilityAgents/Alerts/include/Alerts/Renderer/Renderer.h +++ b/CapabilityAgents/Alerts/include/Alerts/Renderer/Renderer.h @@ -107,12 +107,6 @@ class Renderer : public RendererInterface, public avsCommon::utils::mediaPlayer: /// A flag to capture if renderer is active. bool m_isRendering; - /// The time when the rendering started. - std::chrono::steady_clock::time_point m_timeRenderingStarted; - - /// A flag to capture if the renderer has been asked to stop. - bool m_isStopping; - /// The @c Executor which serially and asynchronously handles operations with regard to rendering the alert. /// TODO : ACSDK-388 to update the onPlayback* callback functions to also go through the executor. avsCommon::utils::threading::Executor m_executor; diff --git a/CapabilityAgents/Alerts/include/Alerts/Renderer/RendererObserverInterface.h b/CapabilityAgents/Alerts/include/Alerts/Renderer/RendererObserverInterface.h index 869ceb3cbb..c5d6a76720 100644 --- a/CapabilityAgents/Alerts/include/Alerts/Renderer/RendererObserverInterface.h +++ b/CapabilityAgents/Alerts/include/Alerts/Renderer/RendererObserverInterface.h @@ -40,8 +40,6 @@ class RendererObserverInterface { STARTED, /// The renderer has stopped rendering due to being stopped via a direct api call. STOPPED, - /// The renderer has stopped due to completion of playing assets, or by an internal timeout. - COMPLETED, /// The renderer has encountered an error. ERROR }; diff --git a/CapabilityAgents/Alerts/src/Alert.cpp b/CapabilityAgents/Alerts/src/Alert.cpp index 083183069a..9dcd35c303 100644 --- a/CapabilityAgents/Alerts/src/Alert.cpp +++ b/CapabilityAgents/Alerts/src/Alert.cpp @@ -45,15 +45,16 @@ using namespace rapidjson; /// String for lookup of the token value in a parsed JSON document. static const std::string KEY_TOKEN = "token"; + /// String for lookup of the scheduled time value in a parsed JSON document. static const std::string KEY_SCHEDULED_TIME = "scheduledTime"; + +/// We won't allow an alert to render more than 1 hour. +const std::chrono::seconds MAXIMUM_ALERT_RENDERING_TIME = std::chrono::hours(1); + /// String to identify log entries originating from this file. static const std::string TAG("Alert"); -/// A file-static variable to control if simple mode is enabled. This variable, and all the logic it controls when -/// true, will be deleted once our gstreamer implementation is improved. -static const bool AVS_ALERTS_SIMPLE_MODE_ENABLED = true; - /** * Create a LogEntry using this file's TAG and the specified event string. * @@ -125,7 +126,7 @@ std::string Alert::parseFromJsonStatusToString(Alert::ParseFromJsonStatus parseF Alert::Alert() : m_dbId{0}, m_scheduledTime_Unix{0}, m_state{State::SET}, m_rendererState{RendererObserverInterface::State::UNSET}, m_stopReason{StopReason::UNSET}, - m_focusState{avsCommon::avs::FocusState::NONE}, m_observer{nullptr} { + m_focusState{avsCommon::avs::FocusState::NONE}, m_observer{nullptr}, m_hasTimerExpired{false} { } @@ -191,31 +192,31 @@ void Alert::reset() { } void Alert::activate() { - if (isSimpleModeEnabled()) { - startRendererSimple(); + if (Alert::State::ACTIVATING == m_state || Alert::State::ACTIVE == m_state) { + ACSDK_ERROR(LX("activateFailed").m("Alert is already active.")); return; } m_state = Alert::State::ACTIVATING; + + if (!m_maxLengthTimer.isActive()) { + if (!m_maxLengthTimer.start( + MAXIMUM_ALERT_RENDERING_TIME, std::bind(&Alert::onMaxTimerExpiration, this)).valid()) { + ACSDK_ERROR(LX("executeStartFailed").d("reason", "startTimerFailed")); + } + } + startRenderer(); } void Alert::deActivate(StopReason reason) { - if (isSimpleModeEnabled()) { - stopRendererSimple(); - return; - } - m_state = Alert::State::STOPPING; m_stopReason = reason; + m_maxLengthTimer.stop(); m_renderer->stop(); } void Alert::onRendererStateChange(RendererObserverInterface::State state, const std::string & reason) { - if (isSimpleModeEnabled()) { - return; - } - switch (state) { case RendererObserverInterface::State::UNSET: // no-op @@ -232,27 +233,26 @@ void Alert::onRendererStateChange(RendererObserverInterface::State state, const break; case RendererObserverInterface::State::STOPPED: - if (Alert::State::STOPPING == m_state) { - m_state = State::STOPPED; + if (m_hasTimerExpired) { + m_state = State::COMPLETED; if (m_observer) { - m_observer->onAlertStateChange(m_token, AlertObserverInterface::State::STOPPED); + m_observer->onAlertStateChange(m_token, AlertObserverInterface::State::COMPLETED); } - } else if (Alert::State::SNOOZING == m_state) { - m_state = State::SNOOZED; - if (m_observer) { - m_observer->onAlertStateChange(m_token, AlertObserverInterface::State::SNOOZED); + } else { + if (Alert::State::STOPPING == m_state) { + m_state = State::STOPPED; + if (m_observer) { + m_observer->onAlertStateChange(m_token, AlertObserverInterface::State::STOPPED); + } + } else if (Alert::State::SNOOZING == m_state) { + m_state = State::SNOOZED; + if (m_observer) { + m_observer->onAlertStateChange(m_token, AlertObserverInterface::State::SNOOZED); + } } } break; - case RendererObserverInterface::State::COMPLETED: - m_state = State::COMPLETED; - if (m_observer) { - m_observer->onAlertStateChange(m_token, AlertObserverInterface::State::COMPLETED); - } - - break; - case RendererObserverInterface::State::ERROR: if (m_observer) { m_observer->onAlertStateChange(m_token, AlertObserverInterface::State::ERROR, reason); @@ -297,11 +297,6 @@ void Alert::startRenderer() { fileName = getDefaultShortAudioFilePath(); } - if (isSimpleModeEnabled()) { - startRendererSimple(); - return; - } - m_renderer->setObserver(this); m_renderer->start(fileName); } @@ -319,43 +314,18 @@ void Alert::snooze(const std::string & updatedScheduledTime_ISO_8601) { m_state = State::SNOOZING; m_renderer->stop(); - - if (isSimpleModeEnabled()) { - m_state = State::SET; - } } Alert::StopReason Alert::getStopReason() const { return m_stopReason; } -void Alert::startRendererSimple() { - if (avsCommon::avs::FocusState::BACKGROUND == m_focusState) { - ACSDK_INFO(LX("Alert in background - not playing file.")); - return; - } - - m_renderer->stop(); - m_renderer->setObserver(this); - m_renderer->start(getDefaultAudioFilePath()); -} - -void Alert::stopRendererSimple() { - m_state = Alert::State::STOPPED; - m_stopReason = Alert::StopReason::AVS_STOP; +void Alert::onMaxTimerExpiration() { + m_state = Alert::State::STOPPING; + m_hasTimerExpired = true; m_renderer->stop(); } -bool Alert::isSimpleModeEnabled() { - return AVS_ALERTS_SIMPLE_MODE_ENABLED; -} - -void Alert::activateSimple(avsCommon::avs::FocusState focusState) { - m_focusState = focusState; - m_state = Alert::State::ACTIVE; - startRendererSimple(); -} - } // namespace alerts } // namespace capabilityAgents } // namespace alexaClientSDK \ No newline at end of file diff --git a/CapabilityAgents/Alerts/src/AlertsCapabilityAgent.cpp b/CapabilityAgents/Alerts/src/AlertsCapabilityAgent.cpp index 99b4a3b0be..2a18b8ccb1 100644 --- a/CapabilityAgents/Alerts/src/AlertsCapabilityAgent.cpp +++ b/CapabilityAgents/Alerts/src/AlertsCapabilityAgent.cpp @@ -276,15 +276,13 @@ void AlertsCapabilityAgent::handleDirectiveImmediately(std::shared_ptr info) { - ACSDK_ERROR(LX("preHandleDirective").m("unexpected call.")); + m_caExecutor.submit([this, info]() { executeHandleDirectiveImmediately(info); }); } void AlertsCapabilityAgent::handleDirective(std::shared_ptr info) { - ACSDK_ERROR(LX("handleDirective").m("unexpected call.")); } void AlertsCapabilityAgent::cancelDirective(std::shared_ptr info) { - ACSDK_ERROR(LX("cancelDirective").m("unexpected call.")); } void AlertsCapabilityAgent::onDeregistered() { @@ -406,6 +404,7 @@ void AlertsCapabilityAgent::onAlertStateChange(const std::string &alertToken, Al sendEvent(ALERT_STARTED_EVENT_NAME, m_activeAlert->getToken()); m_activeAlert->setStateActive(); m_alertStorage->modify(m_activeAlert); + updateContextManagerLocked(); } break; @@ -607,11 +606,15 @@ bool AlertsCapabilityAgent::initializeAlerts(const ConfigurationNode &configurat } } + int64_t unixEpochNow; + if (!getCurrentUnixTime(&unixEpochNow)) { + ACSDK_ERROR(LX("initializeAlertsFailed").d("reason", "could not get current unix time.")); + return false; + } + std::vector> alerts; m_alertStorage->load(&alerts); - auto unixEpochNow = getCurrentUnixTime(); - for (auto &alert : alerts) { if (isAlertPastDue(alert, unixEpochNow)) { m_pastDueAlerts.push_back(alert); @@ -700,26 +703,14 @@ bool AlertsCapabilityAgent::handleSetAlert(const std::shared_ptrgetToken(); + std::lock_guard lock(m_mutex); if (m_activeAlert && (m_activeAlert->getToken() == *alertToken) && (Alert::State::ACTIVE == m_activeAlert->getState())) { - snoozeAlertLocked(m_activeAlert, parsedAlert->getScheduledTime_ISO_8601()); sendEvent(ALERT_STOPPED_EVENT_NAME, m_activeAlert->getToken()); - - // We won't be getting callbacks in simple mode. Handle all work here. - if (Alert::isSimpleModeEnabled()) { - m_activeAlert->reset(); - m_scheduledAlerts.insert(m_activeAlert); - m_activeAlert.reset(); - m_alertRenderer->setObserver(nullptr); - releaseChannel(); - updateContextManagerLocked(); - scheduleNextAlertForRendering(); - } - } else { if (getScheduledAlertByTokenLocked(parsedAlert->getToken())) { // This is the best default behavior. If we send SetAlertFailed for a duplicate Alert, @@ -730,7 +721,11 @@ bool AlertsCapabilityAgent::handleSetAlert(const std::shared_ptrgetScheduledTime_Unix() - timeNow}; @@ -915,15 +915,8 @@ void AlertsCapabilityAgent::activateNextAlertLocked() { m_activeAlert = *(m_scheduledAlerts.begin()); m_scheduledAlerts.erase(m_scheduledAlerts.begin()); - if (Alert::isSimpleModeEnabled()) { - m_activeAlert->activateSimple(m_focusState); - sendEvent(ALERT_STARTED_EVENT_NAME, m_activeAlert->getToken()); - m_alertStorage->modify(m_activeAlert); - updateContextManagerLocked(); - } else { - m_activeAlert->setFocusState(m_focusState); - m_activeAlert->activate(); - } + m_activeAlert->setFocusState(m_focusState); + m_activeAlert->activate(); } std::shared_ptr AlertsCapabilityAgent::getScheduledAlertByTokenLocked(const std::string & token) { @@ -981,7 +974,11 @@ void AlertsCapabilityAgent::releaseChannel() { } void AlertsCapabilityAgent::filterPastDueAlerts() { - auto unixEpochNow = getCurrentUnixTime(); + int64_t unixEpochNow; + if (!getCurrentUnixTime(&unixEpochNow)) { + ACSDK_ERROR(LX("filterPastDueAlertsFailed").d("reason", "could not get current unix time.")); + return; + } std::lock_guard lock(m_mutex); @@ -1005,25 +1002,8 @@ void AlertsCapabilityAgent::filterPastDueAlerts() { } void AlertsCapabilityAgent::deactivateActiveAlertHelper(Alert::StopReason stopReason) { - if (!m_activeAlert) { - return; - } - - m_activeAlert->deActivate(stopReason); - - // We won't get callbacks in simple mode. Handle all work here. - if (Alert::isSimpleModeEnabled()) { - // NOTE: Only send AlertStopped Event if local stop. Otherwise this is done during DeleteAlert handling. - if (Alert::StopReason::LOCAL_STOP == stopReason) { - sendEvent(ALERT_STOPPED_EVENT_NAME, m_activeAlert->getToken()); - } - - m_alertStorage->erase(m_activeAlert); - m_activeAlert.reset(); - m_alertRenderer->setObserver(nullptr); - releaseChannel(); - updateContextManagerLocked(); - scheduleNextAlertForRendering(); + if (m_activeAlert) { + m_activeAlert->deActivate(stopReason); } } diff --git a/CapabilityAgents/Alerts/src/CMakeLists.txt b/CapabilityAgents/Alerts/src/CMakeLists.txt index 3823fc7be7..1f9cd64af8 100644 --- a/CapabilityAgents/Alerts/src/CMakeLists.txt +++ b/CapabilityAgents/Alerts/src/CMakeLists.txt @@ -1,3 +1,5 @@ +add_definitions("-DACSDK_LOG_MODULE=alerts") + add_library(Alerts SHARED Renderer/Renderer.cpp Storage/SQLiteAlertStorage.cpp diff --git a/CapabilityAgents/Alerts/src/Renderer/Renderer.cpp b/CapabilityAgents/Alerts/src/Renderer/Renderer.cpp index 8a0f542935..52b4d9cf0f 100644 --- a/CapabilityAgents/Alerts/src/Renderer/Renderer.cpp +++ b/CapabilityAgents/Alerts/src/Renderer/Renderer.cpp @@ -40,9 +40,6 @@ static const std::string TAG("Renderer"); */ #define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) -/// We won't allow an alert to render more than 1 hour. -const std::chrono::seconds MAXIMUM_ALERT_RENDERING_TIME_SECONDS = std::chrono::hours(1); - std::shared_ptr Renderer::create(std::shared_ptr mediaPlayer) { if (!mediaPlayer) { ACSDK_ERROR(LX("createFailed").m("mediaPlayer parameter was nullptr.")); @@ -56,7 +53,7 @@ std::shared_ptr Renderer::create(std::shared_ptr Renderer::Renderer(std::shared_ptr mediaPlayer) : m_mediaPlayer{mediaPlayer}, m_observer{nullptr}, m_loopCount{0}, m_loopPauseInMilliseconds{0}, - m_isRendering{false}, m_isStopping{false} { + m_isRendering{false} { } @@ -89,7 +86,6 @@ void Renderer::start(const std::string & localAudioFilePath, m_urls = urls; m_loopCount = loopCount; m_loopPauseInMilliseconds = loopPauseInMilliseconds; - m_timeRenderingStarted = std::chrono::steady_clock::now(); lock.unlock(); m_executor.submit([this] () { executeStart(); }); @@ -101,7 +97,6 @@ void Renderer::stop() { void Renderer::executeStart() { std::unique_lock lock(m_mutex); - m_isStopping = false; std::string localAudioFilePathCopy = m_localAudioFilePath; lock.unlock(); @@ -115,12 +110,16 @@ void Renderer::executeStart() { void Renderer::executeStop() { std::unique_lock lock(m_mutex); - m_isStopping = true; bool isRenderingCopy = m_isRendering; + RendererObserverInterface* observerCopy = m_observer; lock.unlock(); if (isRenderingCopy) { m_mediaPlayer->stop(); + } else { + if (observerCopy) { + observerCopy->onRendererStateChange(RendererObserverInterface::State::STOPPED); + } } } @@ -137,31 +136,12 @@ void Renderer::onPlaybackStarted() { void Renderer::onPlaybackFinished() { std::unique_lock lock(m_mutex); - bool isStoppingCopy = m_isStopping; m_isRendering = false; - auto timeRenderingStartedCopy = m_timeRenderingStarted; RendererObserverInterface* observerCopy = m_observer; lock.unlock(); - auto currentTime = std::chrono::steady_clock::now(); - auto secondsRendering = - std::chrono::duration_cast(currentTime - timeRenderingStartedCopy); - - // basic error checking in case clocks are off. - if (secondsRendering.count() < 0) { - secondsRendering = std::chrono::seconds::zero(); - ACSDK_ERROR(LX("onPlaybackFinished").m("time rendering has been evaluated to less than zero.")); - } - - if (secondsRendering > MAXIMUM_ALERT_RENDERING_TIME_SECONDS) { - m_mediaPlayer->stop(); - if (observerCopy) { - observerCopy->onRendererStateChange(RendererObserverInterface::State::COMPLETED); - } - } else if (isStoppingCopy) { - if (observerCopy) { - observerCopy->onRendererStateChange(RendererObserverInterface::State::STOPPED); - } + if (observerCopy) { + observerCopy->onRendererStateChange(RendererObserverInterface::State::STOPPED); } } diff --git a/CapabilityAgents/AudioPlayer/CMakeLists.txt b/CapabilityAgents/AudioPlayer/CMakeLists.txt new file mode 100644 index 0000000000..c4e60e6610 --- /dev/null +++ b/CapabilityAgents/AudioPlayer/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.1 FATAL_ERROR) +project(AudioPlayer LANGUAGES CXX) + +include(../../build/BuildDefaults.cmake) + +add_subdirectory("src") diff --git a/CapabilityAgents/AudioPlayer/include/AudioPlayer/AudioItem.h b/CapabilityAgents/AudioPlayer/include/AudioPlayer/AudioItem.h new file mode 100644 index 0000000000..1c8410dc95 --- /dev/null +++ b/CapabilityAgents/AudioPlayer/include/AudioPlayer/AudioItem.h @@ -0,0 +1,97 @@ +/* + * AudioItem.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_AUDIO_ITEM_H_ +#define ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_AUDIO_ITEM_H_ + +#include "StreamFormat.h" + +namespace alexaClientSDK { +namespace capabilityAgents { +namespace audioPlayer { + +/// Struct which contains all the fields which define an audio item for a @c Play directive. +struct AudioItem { + /// Identifies the @c audioItem. + std::string id; + + /// Contains the parameters of the stream. + struct Stream { + /** + * Identifies the location of audio content. If the audio content is a binary audio attachment, the value will + * be a unique identifier for the content, which is formatted as follows: @c "cid:". Otherwise the value will + * be a remote http/https location. + */ + std::string url; + + /** + * The attachment reader for @c url if the audio content is a binary audio attachment. For http/https + * attachments, this field is set to @c nullptr and unused. + */ + std::shared_ptr reader; + + /** + * This field is defined when the @c AudioItem has an associated binary audio attachment. This parameter is + * ignored if the associated audio is a stream. + */ + StreamFormat format; + + /** + * A timestamp indicating where in the stream the client must start playback. For example, when offset is set + * to 0, this indicates playback of the stream must start at 0, or the start of the stream. Any other value + * indicates that playback must start from the provided offset. + */ + std::chrono::milliseconds offset; + + /// The date and time in ISO 8601 format for when the stream becomes invalid. + std::chrono::steady_clock::time_point expiryTime; + + /// Contains values for progress reports. + struct ProgressReport { + + /** + * Specifies when to send the @c ProgressReportDelayElapsed event to AVS. @c ProgressReportDelayElapsed + * must only be sent once at the specified interval. + * + * @note Some music providers do not require this report. If the report is not required, @c delay will be + * set to @c std:chrono::milliseconds::max(). + */ + std::chrono::milliseconds delay; + + /** + * Specifies when to emit a @c ProgressReportIntervalElapsed event to AVS. + * @c ProgressReportIntervalElapsed must be sent periodically at the specified interval. + * + * @note Some music providers do not require this report. If the report is not required, @c interval will + * be set to @c std::chrono::milliseconds::max(). + */ + std::chrono::milliseconds interval; + } progressReport; + + /// An opaque token that represents the current stream. + std::string token; + + /// An opaque token that represents the expected previous stream. + std::string expectedPreviousToken; + } stream; +}; + +} // namespace audioPlayer +} // namespace capabilityAgents +} // namespace alexaClientSDK + +#endif //ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_AUDIO_ITEM_H_ diff --git a/CapabilityAgents/AudioPlayer/include/AudioPlayer/AudioPlayer.h b/CapabilityAgents/AudioPlayer/include/AudioPlayer/AudioPlayer.h new file mode 100644 index 0000000000..6161b6ccd0 --- /dev/null +++ b/CapabilityAgents/AudioPlayer/include/AudioPlayer/AudioPlayer.h @@ -0,0 +1,419 @@ +/* + * AudioPlayer.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_AUDIO_PLAYER_H_ +#define ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_AUDIO_PLAYER_H_ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "AudioItem.h" +#include "ClearBehavior.h" +#include "ErrorType.h" +#include "PlayBehavior.h" +#include "PlayerActivity.h" + +namespace alexaClientSDK { +namespace capabilityAgents { +namespace audioPlayer { + +/** + * This class implements the @c AudioPlayer capability agent. + * + * @see https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/audioplayer + * + * @note For instances of this class to be cleaned up correctly, @c shutdown() must be called. + */ +class AudioPlayer : + public avsCommon::avs::CapabilityAgent, + public avsCommon::utils::mediaPlayer::MediaPlayerObserverInterface, + public std::enable_shared_from_this { +public: + /** + * Creates a new @c AudioPlayer instance. + * + * @param mediaPlayer The instance of the @c MediaPlayerInterface used to play audio. + * @param messageSender The object to use for sending events. + * @param focusManager The channel focus manager used to manage usage of the dialog channel. + * @param contextManager The AVS Context manager used to generate system context for events. + * @param attachmentManager The instance of the @c AttachmentManagerInterface to use to read the attachment. + * @param exceptionSender The object to use for sending AVS Exception messages. + * @return A @c std::shared_ptr to the new @c AudioPlayer instance. + */ + static std::shared_ptr create( + std::shared_ptr mediaPlayer, + std::shared_ptr messageSender, + std::shared_ptr focusManager, + std::shared_ptr contextManager, + std::shared_ptr attachmentManager, + std::shared_ptr exceptionSender); + + /// Prepares/enables the @c AudioPlayer to be deleted. + void shutdown(); + + /// @name StateProviderInterface Functions + /// @{ + void provideState(unsigned int stateRequestToken) override; + /// @} + + /// @name CapabilityAgent/DirectiveHandlerInterface Functions + /// @{ + void handleDirectiveImmediately(std::shared_ptr directive) override; + void preHandleDirective(std::shared_ptr info) override; + void handleDirective(std::shared_ptr info) override; + void cancelDirective(std::shared_ptr info) override; + void onDeregistered() override; + avsCommon::avs::DirectiveHandlerConfiguration getConfiguration() const override; + /// @} + + /// @name ChannelObserverInterface Functions + /// @{ + void onFocusChanged(avsCommon::avs::FocusState newFocus) override; + /// @} + + /// @name MediaPlayerObserverInterface Functions + /// @{ + void onPlaybackStarted() override; + void onPlaybackFinished() override; + void onPlaybackError(std::string error) override; + void onPlaybackPaused() override; + void onPlaybackResumed() override; + void onBufferUnderrun() override; + void onBufferRefilled() override; + /// @} + +private: + /** + * Constructor. + * + * @param mediaPlayer The instance of the @c MediaPlayerInterface used to play audio. + * @param messageSender The object to use for sending events. + * @param focusManager The channel focus manager used to manage usage of the dialog channel. + * @param contextManager The AVS Context manager used to generate system context for events. + * @param attachmentManager The instance of the @c AttachmentManagerInterface to use to read the attachment. + * @param exceptionSender The object to use for sending AVS Exception messages. + * @return A @c std::shared_ptr to the new @c AudioPlayer instance. + */ + AudioPlayer( + std::shared_ptr mediaPlayer, + std::shared_ptr messageSender, + std::shared_ptr focusManager, + std::shared_ptr contextManager, + std::shared_ptr attachmentManager, + std::shared_ptr exceptionSender); + + /** + * This function deserializes a @c Directive's payload into a @c rapidjson::Document. + * + * @param info The @c DirectiveInfo to read the payload string from. + * @param[out] document The @c rapidjson::Document to parse the payload into. + * @return @c true if parsing was successful, else @c false. + */ + bool parseDirectivePayload(std::shared_ptr info, rapidjson::Document* document); + + /** + * This function handles a @c PLAY directive. + * + * @param info The @c DirectiveInfo containing the @c AVSDirective and the @c DirectiveHandlerResultInterface. + */ + void handlePlayDirective(std::shared_ptr info); + + /** + * This function handles a @c STOP directive. + * + * @param info The @c DirectiveInfo containing the @c AVSDirective and the @c DirectiveHandlerResultInterface. + */ + void handleStopDirective(std::shared_ptr info); + + /** + * This function handles a @c CLEAR_QUEUE directive. + * + * @param info The @c DirectiveInfo containing the @c AVSDirective and the @c DirectiveHandlerResultInterface. + */ + void handleClearQueueDirective(std::shared_ptr info); + + /** + * Remove a directive from the map of message IDs to DirectiveInfo instances. + * + * @param info The @c DirectiveInfo containing the @c AVSDirective whose message ID is to be removed. + */ + void removeDirective(std::shared_ptr info); + + /** + * @name Executor Thread Functions + * + * These functions (and only these functions) are called by @c m_executor on a single worker thread. All other + * functions in this class can be called asynchronously, and pass data to the @c Executor thread through parameters + * to lambda functions. No additional synchronization is needed. + */ + /// @{ + + /** + * This function provides updated context information for @c AudioPlayer to @c ContextManager. This function is + * called when @c ContextManager calls @c provideState(), and is also called internally by @c changeActivity(). + * + * @param sendToken flag indicating whether @c stateRequestToken contains a valid token which should be passed + * along to @c ContextManager. This flag defaults to @c false. + * @param stateRequestToken The token @c ContextManager passed to the @c provideState() call, which will be passed + * along to the ContextManager::setState() call. This parameter is not used if @c sendToken is @c false. + */ + void executeProvideState(bool sendToken = false, unsigned int stateRequestToken = 0); + + /** + * This function is called when the @c FocusManager focus changes. + * + * @li If focus changes to @c FOREGROUND after a @c Play directive requested focus, @c AudioPlayer will start + * playing. + * @li If focus changes to @c BACKGROUND while playing (when another component acquires focus on a higher-priority + * channel), @c AudioPlayer will pause playback until it regains @c FOREGROUND focus. + * @li If focus changes to @c FOREGROUND while paused, @c AudioPlayer will resume playing. + * @li If focus changes to @c NONE, all playback will be stopped. + * + * @param newFocus The focus state to change to. + */ + void executeOnFocusChanged(avsCommon::avs::FocusState newFocus); + + /// Handle notification that audio playback has started. + void executeOnPlaybackStarted(); + + /// Handle notification that audio playback has finished. + void executeOnPlaybackFinished(); + + /** + * Handle notification that audio playback encountered an error. + * + * @param error Text describing the nature of the error. + */ + void executeOnPlaybackError(std::string error); + + /// Handle notification that audio playback has paused. + void executeOnPlaybackPaused(); + + /// Handle notification that audio playback has resumed after being paused. + void executeOnPlaybackResumed(); + + /// Handle notification that audio playback has run out of data in the audio buffer. + void executeOnBufferUnderrun(); + + /// Handle notification that audio playback has resumed after encountering a buffer underrun. + void executeOnBufferRefilled(); + + /** + * This function executes a parsed @c PLAY directive. + * + * @param playBehavior Specifies how @c audioItem should be queued/played. + * @param audioItem The new @c AudioItem to play. + */ + void executePlay(PlayBehavior playBehavior, const AudioItem& audioItem); + + /// This fuction plays the next @c AudioItem in the queue. + void playNextItem(); + + /// This function executes a parsed @c STOP directive. + void executeStop(); + + /** + * This function executes a parsed @c CLEAR_QUEUE directive. + * + * @param clearBehavior Specifies how the queue should be cleared. + */ + void executeClearQueue(ClearBehavior clearBehavior); + + /** + * This function changes the @c AudioPlayer state. All state changes are made by calling this function. + * + * @param activity The state to change to. + */ + void changeActivity(PlayerActivity activity); + + /** + * Send the handling completed notification and clean up the resources of @c m_currentInfo. + */ + void setHandlingCompleted(std::shared_ptr info); + + /** + * Send ExceptionEncountered and report a failure to handle the @c AVSDirective. + * + * @param info The @c AVSDirective that encountered the error and ancillary information. + * @param type The type of Exception that was encountered. + * @param message The error message to include in the ExceptionEncountered message. + */ + void sendExceptionEncounteredAndReportFailed( + std::shared_ptr info, + const std::string& message, + avsCommon::avs::ExceptionErrorType type = avsCommon::avs::ExceptionErrorType::INTERNAL_ERROR); + + /** + * Most of the @c AudioPlayer events use the same payload, and only vary in their event name. This utility + * function constructs and sends these generic @c AudioPlayer events. + * + * @param name The name of the event to send. + */ + void sendEventWithTokenAndOffset(const std::string& eventName); + + /// Send a @c PlaybackStarted event. + void sendPlaybackStartedEvent(); + + /// Send a @c PlaybackNearlyFinished event. + void sendPlaybackNearlyFinishedEvent(); + + /// Send a @c ProgressReportDelayElapsed event. + void sendProgressReportDelayElapsedEvent(); + + /// Send a @c ProgressReportIntervalElapsed event. + void sendProgressReportIntervalElapsedEvent(); + + /// Send a @c PlaybackStutterStarted event. + void sendPlaybackStutterStartedEvent(); + + /// Send a @c PlaybackStutterFinished event. + void sendPlaybackStutterFinishedEvent(); + + /// Send a @c PlaybackFinished event. + void sendPlaybackFinishedEvent(); + + /** + * Send a @c PlaybackFailed event. + * + * @param failingToken The token of the playback item that failed. + * @param errorType The cause of the failure. + * @param message A message describing the failure. + */ + void sendPlaybackFailedEvent( + const std::string& failingToken, + ErrorType errorType, + const std::string& message); + + /// Send a @c PlaybackStopped event. + void sendPlaybackStoppedEvent(); + + /// Send a @c PlaybackPaused event. + void sendPlaybackPausedEvent(); + + /// Send a @c PlaybackResumed event. + void sendPlaybackResumedEvent(); + + /// Send a @c PlaybackQueueCleared event. + void sendPlaybackQueueClearedEvent(); + + /// Send a @c PlaybackMetadataExtracted event. + void sendStreamMetadataExtractedEvent(); + + /// Get the media player offset. + std::chrono::milliseconds getMediaPlayerOffset(); + + /// @} + + /// MediaPlayerInterface instance to send audio attachments to. + std::shared_ptr m_mediaPlayer; + + /// The object to use for sending events. + std::shared_ptr m_messageSender; + + /// The @c FocusManager used to manage usage of the dialog channel. + std::shared_ptr m_focusManager; + + /// The @c ContextManager that needs to be updated of the state. + std::shared_ptr m_contextManager; + + /// The @c AttachmentManager used to read attachments. + std::shared_ptr m_attachmentManager; + + /** + * @name Playback Synchronization Variables + * + * These member variables are used during focus change events to wait for callbacks from @c MediaPlayer. They are + * accessed asychronously by the @c MediaPlayerObserverInterface callbacks, as well as by @c m_executor functions. + * These accesses are synchronized by m_blaybackMutex. + */ + /// @{ + + /// Flag which is set by @c onPlaybackStarted. + bool m_playbackStarted; + + /// Flag which is set by @c onPlaybackPaused. + bool m_playbackPaused; + + /// Flag which is set by @c onPlaybackResumed. + bool m_playbackResumed; + + /// Flag which is set by @c onPlaybackFinished. + bool m_playbackFinished; + + /// @} + + /// Mutex to synchronize access to Playback Synchronization Variables. + std::mutex m_playbackMutex; + + /// Condition variable to signal changes to Playback Synchronization Variables. + std::condition_variable m_playbackConditionVariable; + + /** + * @name Executor Thread Variables + * + * These member variables are only accessed by functions in the @c m_executor worker thread, and do not require any + * synchronization. + */ + /// @{ + + /// The current state of the @c AudioPlayer. + PlayerActivity m_currentActivity; + + /// Sub-state indicating we're transitioning to @c PLAYING from @c IDLE/STOPPED/FINISHED + bool m_starting; + + /// The current focus state of the @c AudioPlayer on the content channel. + avsCommon::avs::FocusState m_focus; + + /// The queue of @c AudioItems to play. + std::deque m_audioItems; + + /// The token of the currently (or most recently) playing @c AudioItem. + std::string m_token; + + /// When in the @c BUFFER_UNDERRUN state, this records the time at which the state was entered. + std::chrono::steady_clock::time_point m_bufferUnderrunTimestamp; + + /// This timer is used to send @c ProgressReportDelayElapsed events. + avsCommon::utils::timing::Timer m_delayTimer; + + /// This timer is used to send @c ProgressReportIntervalElapsed events. + avsCommon::utils::timing::Timer m_intervalTimer; + + /// @} + + /** + * @c Executor which queues up operations from asynchronous API calls. + * + * @note This declaration needs to come *after* the Executor Thread Variables above so that the thread shuts down + * before the Executor Thread Variables are destroyed. + */ + avsCommon::utils::threading::Executor m_executor; +}; + +} // namespace audioPlayer +} // namespace capabilityAgents +} // namespace alexaClientSDK + +#endif //ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_AUDIO_PLAYER_H_ diff --git a/CapabilityAgents/AudioPlayer/include/AudioPlayer/ClearBehavior.h b/CapabilityAgents/AudioPlayer/include/AudioPlayer/ClearBehavior.h new file mode 100644 index 0000000000..5b33d5bfde --- /dev/null +++ b/CapabilityAgents/AudioPlayer/include/AudioPlayer/ClearBehavior.h @@ -0,0 +1,105 @@ +/* + * ClearBehavior.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_CLEAR_BEHAVIOR_H_ +#define ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_CLEAR_BEHAVIOR_H_ + +#include + +#include +#include + +namespace alexaClientSDK { +namespace capabilityAgents { +namespace audioPlayer { + +/// Used to determine clear queue behavior. +enum class ClearBehavior { + /// Clears the queue and continues to play the currently playing stream. + CLEAR_ENQUEUED, + + /// Clears the entire playback queue and stops the currently playing stream (if applicable). + CLEAR_ALL +}; + +/** + * Convert a @c ClearBehavior to an AVS-compliant @c std::string. + * + * @param clearBehavior The @c ClearBehavior to convert. + * @return The AVS-compliant string representation of @c clearBehavior. + */ +inline std::string clearBehaviorToString(ClearBehavior clearBehavior) { + switch (clearBehavior) { + case ClearBehavior::CLEAR_ENQUEUED: + return "CLEAR_ENQUEUED"; + case ClearBehavior::CLEAR_ALL: + return "CLEAR_ALL"; + } + return "unknown ClearBehavior"; +} + +/** + * Convert an AVS-compliant @c string to a @c ClearBehavior. + * + * @param text The string to convert. + * @param[out] clearBehavior The converted @c ClearBehavior. + * @return @c true if the string converted succesfully, else @c false. + */ +inline bool stringToClearBehavior(const std::string& text, ClearBehavior * clearBehavior) { + if (nullptr == clearBehavior) { + return false; + } else if (clearBehaviorToString(ClearBehavior::CLEAR_ENQUEUED) == text) { + *clearBehavior = ClearBehavior::CLEAR_ENQUEUED; + return true; + } else if (clearBehaviorToString(ClearBehavior::CLEAR_ALL) == text) { + *clearBehavior = ClearBehavior::CLEAR_ALL; + return true; + } + return false; +} + +/** + * Write a @c ClearBehavior value to an @c ostream. + * + * @param stream The stream to write the value to. + * @param clearBehavior The @c ClearBehavior value to write to the @c ostream as a string. + * @return The @c ostream that was passed in and written to. + */ +inline std::ostream& operator<<(std::ostream& stream, const ClearBehavior& clearBehavior) { + return stream << clearBehaviorToString(clearBehavior); +} + +/** + * Convert a @c ClearBehavior to a @c rapidjson::Value. + * + * @param documentNode The @c rapidjson::Value to write to. + * @param clearBehavior The @c ClearBehavior to convert. + * @return @c true if conversion is successful, else @c false. + */ +inline bool convertToValue(const rapidjson::Value& documentNode, ClearBehavior* clearBehavior) { + std::string text; + if (!avsCommon::utils::json::jsonUtils::convertToValue(documentNode, &text)) { + return false; + } + return stringToClearBehavior(text, clearBehavior); +} + +} // namespace audioPlayer +} // namespace capabilityAgents +} // namespace alexaClientSDK + +#endif //ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_CLEAR_BEHAVIOR_H_ diff --git a/CapabilityAgents/AudioPlayer/include/AudioPlayer/ErrorType.h b/CapabilityAgents/AudioPlayer/include/AudioPlayer/ErrorType.h new file mode 100644 index 0000000000..37407c7052 --- /dev/null +++ b/CapabilityAgents/AudioPlayer/include/AudioPlayer/ErrorType.h @@ -0,0 +1,78 @@ +/* + * ErrorType.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_ERROR_TYPE_H_ +#define ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_ERROR_TYPE_H_ + +#include + +namespace alexaClientSDK { +namespace capabilityAgents { +namespace audioPlayer { + +/// Identifies the specific type of error in a @c PlaybackFailed event. +enum class ErrorType { + /// An unknown error occurred. + MEDIA_ERROR_UNKNOWN, + /// The server recognized the request as being malformed (bad request, unauthorized, forbidden, not found, etc). + MEDIA_ERROR_INVALID_REQUEST, + /// The client was unable to reach the service. + MEDIA_ERROR_SERVICE_UNAVAILABLE, + /// The server accepted the request, but was unable to process the request as expected. + MEDIA_ERROR_INTERNAL_SERVER_ERROR, + /// There was an internal error on the client. + MEDIA_ERROR_INTERNAL_DEVICE_ERROR +}; + +/** + * Convert an @c ErrorType to an AVS-compliant @c std::string. + * + * @param errorType The @c ErrorType to convert. + * @return The AVS-compliant string representation of @c errorType. + */ +inline std::string errorTypeToString(ErrorType errorType) { + switch (errorType) { + case ErrorType::MEDIA_ERROR_UNKNOWN: + return "MEDIA_ERROR_UNKNOWN"; + case ErrorType::MEDIA_ERROR_INVALID_REQUEST: + return "MEDIA_ERROR_INVALID_REQUEST"; + case ErrorType::MEDIA_ERROR_SERVICE_UNAVAILABLE: + return "MEDIA_ERROR_SERVICE_UNAVAILABLE"; + case ErrorType::MEDIA_ERROR_INTERNAL_SERVER_ERROR: + return "MEDIA_ERROR_INTERNAL_SERVER_ERROR"; + case ErrorType::MEDIA_ERROR_INTERNAL_DEVICE_ERROR: + return "MEDIA_ERROR_INTERNAL_DEVICE_ERROR"; + } + return "unknown ErrorType"; +} + +/** + * Write an @c ErrorType value to an @c ostream. + * + * @param stream The stream to write the value to. + * @param errorType The @c ErrorType value to write to the @c ostream as a string. + * @return The @c ostream that was passed in and written to. + */ +inline std::ostream& operator<<(std::ostream& stream, const ErrorType& errorType) { + return stream << errorTypeToString(errorType); +} + +} // namespace audioPlayer +} // namespace capabilityAgents +} // namespace alexaClientSDK + +#endif //ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_ERROR_TYPE_H_ diff --git a/CapabilityAgents/AudioPlayer/include/AudioPlayer/PlayBehavior.h b/CapabilityAgents/AudioPlayer/include/AudioPlayer/PlayBehavior.h new file mode 100644 index 0000000000..3939612817 --- /dev/null +++ b/CapabilityAgents/AudioPlayer/include/AudioPlayer/PlayBehavior.h @@ -0,0 +1,116 @@ +/* + * PlayBehavior.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_PLAY_BEHAVIOR_H_ +#define ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_PLAY_BEHAVIOR_H_ + +#include + +#include +#include + +namespace alexaClientSDK { +namespace capabilityAgents { +namespace audioPlayer { + +/// Used to determine how a client must handle queueing and playback of a stream. +enum class PlayBehavior { + /** + * Immediately begin playback of the stream returned with the @c Play directive, and replace current and enqueued + * streams. + */ + REPLACE_ALL, + + /// Adds a stream to the end of the current queue. + ENQUEUE, + + /// Replace all streams in the queue. This does not impact the currently playing stream. + REPLACE_ENQUEUED +}; + +/** + * Convert a @c PlayBehavior to an AVS-compliant @c std::string. + * + * @param playBehavior The @c PlayBehavior to convert. + * @return The AVS-compliant string representation of @c playBehavior. + */ +inline std::string playBehaviorToString(PlayBehavior playBehavior) { + switch (playBehavior) { + case PlayBehavior::REPLACE_ALL: + return "REPLACE_ALL"; + case PlayBehavior::ENQUEUE: + return "ENQUEUE"; + case PlayBehavior::REPLACE_ENQUEUED: + return "REPLACE_ENQUEUED"; + } + return "unknown PlayBehavior"; +} + +/** + * Convert an AVS-compliant @c string to a @c PlayBehavior. + * + * @param text The string to convert. + * @param[out] playBehavior The converted @c PlayBehavior. + * @return @c true if the string converted succesfully, else @c false. + */ +inline bool stringToPlayBehavior(const std::string& text, PlayBehavior * playBehavior) { + if (nullptr == playBehavior) { + return false; + } else if (playBehaviorToString(PlayBehavior::REPLACE_ALL) == text) { + *playBehavior = PlayBehavior::REPLACE_ALL; + return true; + } else if (playBehaviorToString(PlayBehavior::ENQUEUE) == text) { + *playBehavior = PlayBehavior::ENQUEUE; + return true; + } else if (playBehaviorToString(PlayBehavior::REPLACE_ENQUEUED) == text) { + *playBehavior = PlayBehavior::REPLACE_ENQUEUED; + return true; + } + return false; +} + +/** + * Write a @c PlayBehavior value to an @c ostream. + * + * @param stream The stream to write the value to. + * @param playBehavior The @c PlayBehavior value to write to the @c ostream as a string. + * @return The @c ostream that was passed in and written to. + */ +inline std::ostream& operator<<(std::ostream& stream, const PlayBehavior& playBehavior) { + return stream << playBehaviorToString(playBehavior); +} + +/** + * Convert a @c PlayBehavior to a @c rapidjson::Value. + * + * @param documentNode The @c rapidjson::Value to write to. + * @param playBehavior The @c PlayBehavior to convert. + * @return @c true if conversion is successful, else @c false. + */ +inline bool convertToValue(const rapidjson::Value& documentNode, PlayBehavior* playBehavior) { + std::string text; + if (!avsCommon::utils::json::jsonUtils::convertToValue(documentNode, &text)) { + return false; + } + return stringToPlayBehavior(text, playBehavior); +} + +} // namespace audioPlayer +} // namespace capabilityAgents +} // namespace alexaClientSDK + +#endif //ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_PLAY_BEHAVIOR_H_ diff --git a/CapabilityAgents/AudioPlayer/include/AudioPlayer/PlayerActivity.h b/CapabilityAgents/AudioPlayer/include/AudioPlayer/PlayerActivity.h new file mode 100644 index 0000000000..96e0bf71e2 --- /dev/null +++ b/CapabilityAgents/AudioPlayer/include/AudioPlayer/PlayerActivity.h @@ -0,0 +1,90 @@ +/* + * PlayerActivity.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_PLAYER_ACTIVITY_H_ +#define ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_PLAYER_ACTIVITY_H_ + +#include + +namespace alexaClientSDK { +namespace capabilityAgents { +namespace audioPlayer { + +/// Identifies the player state. +enum class PlayerActivity { + /// Initial state, prior to acting on the first @c Play directive. + IDLE, + + /// Indicates that audio is currently playing. + PLAYING, + + /** + * Indicates that audio playback was stopped due to an error or a directive which stops or replaces the current + * stream. + */ + STOPPED, + + /// Indicates that the audio stream has been paused. + PAUSED, + + /// Indicates that a buffer underrun has occurred and the stream is buffering. + BUFFER_UNDERRUN, + + /// Indicates that playback has finished. + FINISHED +}; + +/* + * Convert a @c PlayerActivity to an AVS-compliant @c std::string. + * + * @param playerActivity The @c PlayerActivity to convert. + * @return The AVS-compliant string representation of @c playerActivity. + */ +inline std::string playerActivityToString(PlayerActivity playerActivity) { + switch (playerActivity) { + case PlayerActivity::IDLE: + return "IDLE"; + case PlayerActivity::PLAYING: + return "PLAYING"; + case PlayerActivity::STOPPED: + return "STOPPED"; + case PlayerActivity::PAUSED: + return "PAUSED"; + case PlayerActivity::BUFFER_UNDERRUN: + return "BUFFER_UNDERRUN"; + case PlayerActivity::FINISHED: + return "FINISHED"; + } + return "unknown PlayerActivity"; +} + +/** + * Write a @c PlayerActivity value to an @c ostream. + * + * @param stream The stream to write the value to. + * @param playerActivity The @c PlayerActivity value to write to the @c ostream as a string. + * @return The @c ostream that was passed in and written to. + */ +inline std::ostream& operator<<(std::ostream& stream, const PlayerActivity& playerActivity) { + return stream << playerActivityToString(playerActivity); +} + +} // namespace audioPlayer +} // namespace capabilityAgents +} // namespace alexaClientSDK + +#endif //ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_PLAYER_ACTIVITY_H_ diff --git a/CapabilityAgents/AudioPlayer/include/AudioPlayer/StreamFormat.h b/CapabilityAgents/AudioPlayer/include/AudioPlayer/StreamFormat.h new file mode 100644 index 0000000000..73523244ee --- /dev/null +++ b/CapabilityAgents/AudioPlayer/include/AudioPlayer/StreamFormat.h @@ -0,0 +1,101 @@ +/* + * StreamFormat.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_STREAM_FORMAT_H_ +#define ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_STREAM_FORMAT_H_ + +#include + +#include +#include + +namespace alexaClientSDK { +namespace capabilityAgents { +namespace audioPlayer { + +/// Specifies the format of a binary audio attachment in a @c Play directive. +enum class StreamFormat { + /// Audio is in mpeg format. + AUDIO_MPEG, + /// Audio is an unknown/unsupported format. + UNKNOWN +}; + +/** + * Convert a @c StreamFormat to an AVS-compliant @c std::string. + * + * @param streamFormat The @c StreamFormat to convert. + * @return The AVS-compliant string representation of @c streamFormat. + */ +inline std::string streamFormatToString(StreamFormat streamFormat) { + switch (streamFormat) { + case StreamFormat::AUDIO_MPEG: + return "AUDIO_MPEG"; + case StreamFormat::UNKNOWN: + break; + } + return "unknown StreamFormat"; +} + +/** + * Convert an AVS-compliant @c string to a @c StreamFormat. + * + * @param text The string to convert. + * @param[out] streamFormat The converted @c StreamFormat. + * @return @c true if the string converted succesfully, else @c false. + */ +inline bool stringToStreamFormat(const std::string& text, StreamFormat * streamFormat) { + if (nullptr == streamFormat) { + return false; + } else if (text == streamFormatToString(StreamFormat::AUDIO_MPEG)) { + *streamFormat = StreamFormat::AUDIO_MPEG; + return true; + } + return false; +} + +/** + * Write a @c StreamFormat value to an @c ostream. + * + * @param stream The stream to write the value to. + * @param streamFormat The @c StreamFormat value to write to the @c ostream as a string. + * @return The @c ostream that was passed in and written to. + */ +inline std::ostream& operator<<(std::ostream& stream, const StreamFormat& streamFormat) { + return stream << streamFormatToString(streamFormat); +} + +/** + * Convert a @c StreamFormat to a @c rapidjson::Value. + * + * @param documentNode The @c rapidjson::Value to write to. + * @param streamFormat The @c StreamFormat to convert. + * @return @c true if conversion is successful, else @c false. + */ +inline bool convertToValue(const rapidjson::Value& documentNode, StreamFormat* streamFormat) { + std::string text; + if (!avsCommon::utils::json::jsonUtils::convertToValue(documentNode, &text)) { + return false; + } + return stringToStreamFormat(text, streamFormat); +} + +} // namespace audioPlayer +} // namespace capabilityAgents +} // namespace alexaClientSDK + +#endif //ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_AUDIO_PLAYER_INCLUDE_AUDIO_PLAYER_STREAM_FORMAT_H_ diff --git a/CapabilityAgents/AudioPlayer/src/AudioPlayer.cpp b/CapabilityAgents/AudioPlayer/src/AudioPlayer.cpp new file mode 100644 index 0000000000..9a113f15f1 --- /dev/null +++ b/CapabilityAgents/AudioPlayer/src/AudioPlayer.cpp @@ -0,0 +1,963 @@ +/* + * AudioPlayer.cpp + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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. + */ + +/// @file AudioPlayer.cpp + +#include "AudioPlayer/AudioPlayer.h" + +#include +#include +#include + +#include +#include + +namespace alexaClientSDK { +namespace capabilityAgents { +namespace audioPlayer { + +using namespace avsCommon::avs; +using namespace avsCommon::avs::attachment; +using namespace avsCommon::sdkInterfaces; +using namespace avsCommon::utils; +using namespace avsCommon::utils::json; +using namespace avsCommon::utils::logger; +using namespace avsCommon::utils::mediaPlayer; + +/// String to identify log entries originating from this file. +static const std::string TAG("AudioPlayer"); + +/** + * Create a LogEntry using this file's TAG and the specified event string. + * + * @param The event string for this @c LogEntry. + */ +#define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) + +/// The name of the @c FocusManager channel used by @c AudioPlayer. +static const std::string CHANNEL_NAME = avsCommon::sdkInterfaces::FocusManagerInterface::CONTENT_CHANNEL_NAME; + +/// The activityId string used with @c FocusManager by @c AudioPlayer. +static const std::string ACTIVITY_ID = "AudioPlayer.Play"; + +/// The namespace for this capability agent. +static const std::string NAMESPACE = "AudioPlayer"; + +/// The @c Play directive signature. +static const NamespaceAndName PLAY{NAMESPACE, "Play"}; + +/// The @c Stop directive signature. +static const NamespaceAndName STOP{NAMESPACE, "Stop"}; + +/// The @c ClearQueue directive signature. +static const NamespaceAndName CLEAR_QUEUE{NAMESPACE, "ClearQueue"}; + +/// The @c AudioPlayer context state signature. +static const NamespaceAndName STATE{NAMESPACE, "PlaybackState"}; + +/// Prefix for content ID prefix in the url property of the directive payload. +static const std::string CID_PREFIX{"cid:"}; + +/// The token key used in @c AudioPlayer events. +static const char TOKEN_KEY[] = "token"; + +/// The offset key used in @c AudioPlayer events. +static const char OFFSET_KEY[] = "offsetInMilliseconds"; + +/// The activity key used in @c AudioPlayer events. +static const char ACTIVITY_KEY[] = "playerActivity"; + +/// The stutter key used in @c AudioPlayer events. +static const char STUTTER_DURATION_KEY[] = "stutterDurationInMilliseconds"; + +/// The duration to wait for a state change in @c onFocusChanged before failing. +static const std::chrono::seconds TIMEOUT{2}; + +std::shared_ptr AudioPlayer::create( + std::shared_ptr mediaPlayer, + std::shared_ptr messageSender, + std::shared_ptr focusManager, + std::shared_ptr contextManager, + std::shared_ptr attachmentManager, + std::shared_ptr exceptionSender) { + if (nullptr == mediaPlayer) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullMediaPlayer")); + return nullptr; + } else if (nullptr == messageSender) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullMessageSender")); + return nullptr; + } else if (nullptr == focusManager) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullFocusManager")); + return nullptr; + } else if (nullptr == contextManager) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullContextManager")); + return nullptr; + } else if (nullptr == attachmentManager) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullAttachmentManager")); + return nullptr; + } else if (nullptr == exceptionSender) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullExceptionSender")); + return nullptr; + } + + auto audioPlayer = std::shared_ptr(new AudioPlayer( + mediaPlayer, + messageSender, + focusManager, + contextManager, + attachmentManager, + exceptionSender)); + mediaPlayer->setObserver(audioPlayer); + contextManager->setStateProvider(STATE, audioPlayer); + return audioPlayer; +} + +void AudioPlayer::shutdown() { + m_mediaPlayer->setObserver(nullptr); + m_executor.submit([this] { executeStop(); m_audioItems.clear(); }).wait(); +} + +void AudioPlayer::provideState(unsigned int stateRequestToken) { + m_executor.submit([this, stateRequestToken] { executeProvideState(true, stateRequestToken); }); +} + +void AudioPlayer::handleDirectiveImmediately(std::shared_ptr directive) { + handleDirective(std::make_shared(directive, nullptr)); +} + +void AudioPlayer::preHandleDirective(std::shared_ptr info) { + // TODO: Move as much processing up here as possilble (ACSDK415). +} + +void AudioPlayer::handleDirective(std::shared_ptr info) { + if (!info) { + ACSDK_ERROR(LX("handleDirectiveFailed").d("reason", "nullDirectiveInfo")); + return; + } + if (info->directive->getName() == PLAY.name) { + handlePlayDirective(info); + } else if (info->directive->getName() == STOP.name) { + handleStopDirective(info); + } else if (info->directive->getName() == CLEAR_QUEUE.name) { + handleClearQueueDirective(info); + } else { + m_executor.submit([this, info] { + sendExceptionEncounteredAndReportFailed( + info, + "unexpected directive " + info->directive->getNamespace() + ":" + info->directive->getName(), + ExceptionErrorType::UNEXPECTED_INFORMATION_RECEIVED); + } + ); + ACSDK_ERROR(LX("handleDirectiveFailed") + .d("reason", "unknownDirective") + .d("namespace", info->directive->getNamespace()) + .d("name", info->directive->getName())); + } +} + +void AudioPlayer::cancelDirective(std::shared_ptr info) { + removeDirective(info); +} + +void AudioPlayer::onDeregistered() { + executeStop(); + m_audioItems.clear(); +} + +DirectiveHandlerConfiguration AudioPlayer::getConfiguration() const { + DirectiveHandlerConfiguration configuration; + configuration[PLAY] = BlockingPolicy::NON_BLOCKING; + configuration[STOP] = BlockingPolicy::NON_BLOCKING; + configuration[CLEAR_QUEUE] = BlockingPolicy::NON_BLOCKING; + return configuration; +} + +void AudioPlayer::onFocusChanged(FocusState newFocus) { + ACSDK_DEBUG9(LX("onFocusChanged").d("newFocus", newFocus)); + auto result = m_executor.submit([this, newFocus] { executeOnFocusChanged(newFocus); }); + if (result.wait_for(TIMEOUT) == std::future_status::timeout) { + ACSDK_ERROR(LX("onFocusChangedFailed").d("reason", "timedout").d("cause", "executorTimeout")); + } +} + +void AudioPlayer::onPlaybackStarted() { + ACSDK_DEBUG9(LX("onPlaybackStarted")); + m_executor.submit([this] { executeOnPlaybackStarted(); }); + + std::unique_lock lock(m_playbackMutex); + m_playbackStarted = true; + m_playbackConditionVariable.notify_all(); +} + +void AudioPlayer::onPlaybackFinished() { + ACSDK_DEBUG9(LX("onPlaybackFinished")); + m_executor.submit([this] { executeOnPlaybackFinished(); }); + + std::unique_lock lock(m_playbackMutex); + m_playbackFinished = true; + m_playbackConditionVariable.notify_all(); +} + +void AudioPlayer::onPlaybackError(std::string error) { + ACSDK_DEBUG9(LX("onPlaybackError").d("error", error)); + m_executor.submit([this, error] { executeOnPlaybackError(error); }); +} + +void AudioPlayer::onPlaybackPaused() { + ACSDK_DEBUG9(LX("onPlaybackPaused")); + m_executor.submit([this] { executeOnPlaybackPaused(); }); + + std::unique_lock lock(m_playbackMutex); + m_playbackPaused = true; + m_playbackConditionVariable.notify_all(); +} + +void AudioPlayer::onPlaybackResumed() { + ACSDK_DEBUG9(LX("onPlaybackResumed")); + m_executor.submit([this] { executeOnPlaybackResumed(); }); + + std::unique_lock lock(m_playbackMutex); + m_playbackResumed = true; + m_playbackConditionVariable.notify_all(); +} + +void AudioPlayer::onBufferUnderrun() { + ACSDK_DEBUG9(LX("onBufferUnderrun")); + m_executor.submit([this] { executeOnBufferUnderrun(); }); +} + +void AudioPlayer::onBufferRefilled() { + ACSDK_DEBUG9(LX("onBufferRefilled")); + m_executor.submit([this] { executeOnBufferRefilled(); }); +} + +AudioPlayer::AudioPlayer( + std::shared_ptr mediaPlayer, + std::shared_ptr messageSender, + std::shared_ptr focusManager, + std::shared_ptr contextManager, + std::shared_ptr attachmentManager, + std::shared_ptr exceptionSender) : + CapabilityAgent{NAMESPACE, exceptionSender}, + m_mediaPlayer{mediaPlayer}, + m_messageSender{messageSender}, + m_focusManager{focusManager}, + m_contextManager{contextManager}, + m_attachmentManager{attachmentManager}, + m_playbackStarted{false}, + m_playbackPaused{false}, + m_playbackResumed{false}, + m_playbackFinished{false}, + m_currentActivity{PlayerActivity::IDLE}, + m_starting{false}, + m_focus{FocusState::NONE} { +} + +bool AudioPlayer::parseDirectivePayload(std::shared_ptr info, rapidjson::Document * document) { + rapidjson::ParseResult result = document->Parse(info->directive->getPayload()); + if (result) { + return true; + } + + ACSDK_ERROR(LX("parseDirectivePayloadFailed") + .d("reason", rapidjson::GetParseError_En(result.Code())) + .d("offset", result.Offset()) + .d("messageId", info->directive->getMessageId())); + m_executor.submit([this, info] { + sendExceptionEncounteredAndReportFailed( + info, + "Unable to parse payload", + ExceptionErrorType::UNEXPECTED_INFORMATION_RECEIVED); + }); + return false; +} + +void AudioPlayer::handlePlayDirective(std::shared_ptr info) { + ACSDK_DEBUG9(LX("handlePlayDirective")); + rapidjson::Document payload; + if (!parseDirectivePayload(info, &payload)) { + return; + } + + PlayBehavior playBehavior; + if (!jsonUtils::retrieveValue(payload, "playBehavior", &playBehavior)) { + playBehavior = PlayBehavior::ENQUEUE; + } + + rapidjson::Value::ConstMemberIterator audioItemJson; + if (!jsonUtils::findNode(payload, "audioItem", &audioItemJson)) { + ACSDK_ERROR(LX("handlePlayDirectiveFailed") + .d("reason", "missingAudioItem") + .d("messageId", info->directive->getMessageId())); + m_executor.submit([this, info] { sendExceptionEncounteredAndReportFailed(info, "missing AudioItem"); }); + return; + } + + AudioItem audioItem; + if (!jsonUtils::retrieveValue(audioItemJson->value, "audioItemId", &audioItem.id)) { + audioItem.id = "anonymous"; + } + + rapidjson::Value::ConstMemberIterator stream; + if (!jsonUtils::findNode(audioItemJson->value, "stream", &stream)) { + ACSDK_ERROR(LX("handlePlayDirectiveFailed") + .d("reason", "missingStream") + .d("messageId", info->directive->getMessageId())); + m_executor.submit([this, info] {sendExceptionEncounteredAndReportFailed(info, "missing stream"); }); + return; + } + + if (!jsonUtils::retrieveValue(stream->value, "url", &audioItem.stream.url)) { + ACSDK_ERROR(LX("handlePlayDirectiveFailed") + .d("reason", "missingUrl") + .d("messageId", info->directive->getMessageId())); + m_executor.submit([this, info] { sendExceptionEncounteredAndReportFailed(info, "missing URL"); }); + return; + } + + if (!jsonUtils::retrieveValue(stream->value, "streamFormat", &audioItem.stream.format)) { + // Some streams with attachments are missing a streamFormat field; assume AUDIO_MPEG. + audioItem.stream.format = StreamFormat::AUDIO_MPEG; + } + + if (audioItem.stream.url.compare(0, CID_PREFIX.size(), CID_PREFIX) == 0) { + std::string contentId = audioItem.stream.url.substr(CID_PREFIX.length()); + audioItem.stream.reader = info->directive->getAttachmentReader(contentId, AttachmentReader::Policy::BLOCKING); + if (nullptr == audioItem.stream.reader) { + ACSDK_ERROR(LX("handlePlayDirectiveFailed") + .d("reason", "getAttachmentReaderFailed") + .d("messageId", info->directive->getMessageId())); + m_executor.submit( + [this, info] { + sendExceptionEncounteredAndReportFailed(info, "unable to obtain attachment reader"); + } + ); + return; + } + + //TODO: Add a method to MediaPlayer to query whether a format is supported (ACSDK-416). + if (audioItem.stream.format != StreamFormat::AUDIO_MPEG) { + ACSDK_ERROR(LX("handlePlayDirectiveFailed") + .d("reason", "unsupportedFormat") + .d("format", audioItem.stream.format) + .d("messageId", info->directive->getMessageId())); + std::string message = "unsupported format " + streamFormatToString(audioItem.stream.format); + m_executor.submit([this, info, message] { sendExceptionEncounteredAndReportFailed(info, message); }); + return; + } + } + + int64_t milliseconds; + if (jsonUtils::retrieveValue(stream->value, "offsetInMilliseconds", &milliseconds)) { + audioItem.stream.offset = std::chrono::milliseconds(milliseconds); + } else { + audioItem.stream.offset = std::chrono::milliseconds::zero(); + } + + // TODO : ACSDK-387 should simplify this code + // Note: expiryTime is provided by AVS, but no enforcement of it is required; capturing it here for completeness, + // but it is currently unused. + std::string expiryTimeString; + audioItem.stream.expiryTime = std::chrono::steady_clock::time_point::max(); + if (jsonUtils::retrieveValue(stream->value, "expiryTime", &expiryTimeString)) { + int64_t unixTime; + if (timing::convert8601TimeStringToUnix(expiryTimeString, &unixTime)) { + int64_t currentTime; + if (timing::getCurrentUnixTime(¤tTime)) { + std::chrono::seconds timeToExpiry(unixTime - currentTime); + audioItem.stream.expiryTime = std::chrono::steady_clock::now() + timeToExpiry; + } + } + } + + rapidjson::Value::ConstMemberIterator progressReport; + audioItem.stream.progressReport.delay = std::chrono::milliseconds::max(); + audioItem.stream.progressReport.interval = std::chrono::milliseconds::max(); + if (!jsonUtils::findNode(stream->value, "progressReport", &progressReport)) { + progressReport = stream->value.MemberEnd(); + } else { + if (jsonUtils::retrieveValue( + progressReport->value, + "progressReportDelayInMilliseconds", + &milliseconds)) { + audioItem.stream.progressReport.delay = std::chrono::milliseconds(milliseconds); + } + + if (jsonUtils::retrieveValue( + progressReport->value, + "progressReportIntervalInMilliseconds", + &milliseconds)) { + audioItem.stream.progressReport.interval = std::chrono::milliseconds(milliseconds); + } + } + + if (!jsonUtils::retrieveValue(stream->value, "token", &audioItem.stream.token)) { + audioItem.stream.token = ""; + } + + if (!jsonUtils::retrieveValue(stream->value, "expectedPreviousToken", &audioItem.stream.expectedPreviousToken)) { + audioItem.stream.expectedPreviousToken = ""; + } + + m_executor.submit( + [this, info, playBehavior, audioItem] { + executePlay(playBehavior, audioItem); + + // Note: Unlike SpeechSynthesizer, AudioPlayer directives are instructing the client to start/stop/queue + // content, so directive handling is considered to be complete when we have queued the content for + // playback; we don't wait for playback to complete. + setHandlingCompleted(info); + } + ); +} + +void AudioPlayer::handleStopDirective(std::shared_ptr info) { + ACSDK_DEBUG9(LX("handleStopDirective")); + m_executor.submit( + [this, info] { + setHandlingCompleted(info); + executeStop(); + } + ); +} + +void AudioPlayer::handleClearQueueDirective(std::shared_ptr info) { + ACSDK_DEBUG9(LX("handleClearQueue")); + rapidjson::Document payload; + if (!parseDirectivePayload(info, &payload)) { + return; + } + + ClearBehavior clearBehavior; + if (!jsonUtils::retrieveValue(payload, "clearBehavior", &clearBehavior)) { + clearBehavior = ClearBehavior::CLEAR_ENQUEUED; + } + + m_executor.submit( + [this, info, clearBehavior] { + setHandlingCompleted(info); + executeClearQueue(clearBehavior); + } + ); +} + +void AudioPlayer::removeDirective(std::shared_ptr info) { + // Check result too, to catch cases where DirectiveInfo was created locally, without a nullptr result. + // In those cases there is no messageId to remove because no result was expected. + if (info->directive && info->result) { + CapabilityAgent::removeDirective(info->directive->getMessageId()); + } +} + +void AudioPlayer::executeProvideState(bool sendToken, unsigned int stateRequestToken) { + ACSDK_DEBUG(LX("executeProvideState").d("sendToken", sendToken).d("stateRequestToken", stateRequestToken)); + auto policy = StateRefreshPolicy::NEVER; + if (PlayerActivity::PLAYING == m_currentActivity) { + policy = StateRefreshPolicy::ALWAYS; + } + + rapidjson::Document state(rapidjson::kObjectType); + state.AddMember(TOKEN_KEY, m_token, state.GetAllocator()); + state.AddMember( + OFFSET_KEY, + std::chrono::duration_cast(getMediaPlayerOffset()).count(), + state.GetAllocator()); + state.AddMember(ACTIVITY_KEY, playerActivityToString(m_currentActivity), state.GetAllocator()); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + if (!state.Accept(writer)) { + ACSDK_ERROR(LX("executeProvideState").d("reason", "writerRefusedJsonObject")); + return; + } + + SetStateResult result; + if (sendToken) { + result = m_contextManager->setState(STATE, buffer.GetString(), policy, stateRequestToken); + } else { + result = m_contextManager->setState(STATE, buffer.GetString(), policy); + } + if (result != SetStateResult::SUCCESS) { + ACSDK_ERROR(LX("executeProvideState").d("reason", "contextManagerSetStateFailed").d("token", m_token)); + } +} + +void AudioPlayer::executeOnFocusChanged(FocusState newFocus) { + ACSDK_DEBUG9(LX("executeOnFocusChanged").d("from", m_focus).d("to", newFocus)); + if (m_focus == newFocus) { + return; + } + m_focus = newFocus; + + switch (newFocus) { + case FocusState::FOREGROUND: + if (m_starting) { + std::unique_lock lock(m_playbackMutex); + m_playbackStarted = false; + ACSDK_DEBUG9(LX("executeOnFocusChanged").d("action", "playNextItem")); + playNextItem(); + if (!m_playbackConditionVariable.wait_for(lock, TIMEOUT, [this] { return m_playbackStarted; })) { + ACSDK_ERROR(LX("onFocusChangedFailed").d("reason", "timedout").d("cause", "notStarted")); + } + } else if (PlayerActivity::PAUSED == m_currentActivity) { + std::unique_lock lock(m_playbackMutex); + m_playbackResumed = false; + ACSDK_DEBUG9(LX("executeOnFocusChanged").d("action", "resumeMediaPlayer")); + if (m_mediaPlayer->resume() == MediaPlayerStatus::FAILURE) { + sendPlaybackFailedEvent( + m_token, + ErrorType::MEDIA_ERROR_INTERNAL_DEVICE_ERROR, + "failed to resume media player"); + ACSDK_ERROR(LX("executeOnFocusChangedFailed").d("reason", "resumeFailed")); + m_focusManager->releaseChannel(CHANNEL_NAME, shared_from_this()); + return; + } + if (!m_playbackConditionVariable.wait_for(lock, TIMEOUT, [this] { return m_playbackResumed; })) { + sendPlaybackFailedEvent( + m_token, + ErrorType::MEDIA_ERROR_INTERNAL_DEVICE_ERROR, + "resume media player timed out"); + ACSDK_ERROR(LX("onFocusChangedFailed").d("reason", "timedOut").d("cause", "notResumed")); + m_focusManager->releaseChannel(CHANNEL_NAME, shared_from_this()); + } + } else { + ACSDK_DEBUG9(LX("executeOnFocusChanged") + .d("action", "none") + .d("m_currentActivity", m_currentActivity)); + } + break; + case FocusState::BACKGROUND: + if (PlayerActivity::PLAYING == m_currentActivity) { + std::unique_lock lock(m_playbackMutex); + m_playbackPaused = false; + ACSDK_DEBUG9(LX("executeOnFocusChanged").d("action", "pauseMediaPlayer")); + if (m_mediaPlayer->pause() == MediaPlayerStatus::FAILURE) { + sendPlaybackFailedEvent( + m_token, + ErrorType::MEDIA_ERROR_INTERNAL_DEVICE_ERROR, + "failed to pause media player"); + ACSDK_ERROR(LX("executeOnFocusChangedFailed").d("reason", "pauseFailed")); + return; + } + if (!m_playbackConditionVariable.wait_for(lock, TIMEOUT, [this] { return m_playbackPaused; })) { + sendPlaybackFailedEvent( + m_token, + ErrorType::MEDIA_ERROR_INTERNAL_DEVICE_ERROR, + "pause media player timed out"); + ACSDK_ERROR(LX("onFocusChangedFailed").d("reason", "timedOut").d("cause", "notPaused")); + } + } + break; + case FocusState::NONE: + if (PlayerActivity::STOPPED == m_currentActivity) { + break; + } + m_audioItems.clear(); + + std::unique_lock lock(m_playbackMutex); + m_playbackFinished = false; + + /* Note: MediaPlayer::stop() calls onPlaybackFinished() synchronously, which results in a mutex deadlock + * here if the lock his held for the executeStop() call. Releasing the lock temporarily avoids the + * deadlock. If MediaPlayer is changed in the future to asynchronously call onPlaybackFinished (and + * documented as such in MediaPlayerInterface), the unlock/lock calls can be removed. */ + lock.unlock(); + ACSDK_DEBUG9(LX("executeOnFocusChanged").d("action", "executeStop")); + executeStop(); + lock.lock(); + + if (!m_playbackConditionVariable.wait_for(lock, TIMEOUT, [this] { return m_playbackFinished; })) { + ACSDK_ERROR(LX("onFocusChangedFailed").d("reason", "timedout").d("cause", "notFinished")); + } + break; + } +} + +void AudioPlayer::executeOnPlaybackStarted() { + changeActivity(PlayerActivity::PLAYING); + + sendPlaybackStartedEvent(); + + // TODO: Once MediaPlayer can notify of nearly finished, send there instead (ACSDK-417). + sendPlaybackNearlyFinishedEvent(); +} + +void AudioPlayer::executeOnPlaybackFinished() { + if (m_currentActivity != PlayerActivity::PLAYING ) { + ACSDK_ERROR(LX("executeOnPlaybackFinishedError") + .d("reason", "notPlaying") + .d("m_currentActivity", m_currentActivity)); + return; + } + + if (m_audioItems.empty()) { + changeActivity(PlayerActivity::FINISHED); + sendPlaybackFinishedEvent(); + m_focusManager->releaseChannel(CHANNEL_NAME, shared_from_this()); + return; + } + sendPlaybackFinishedEvent(); + playNextItem(); +} + +void AudioPlayer::executeOnPlaybackError(std::string error) { + ACSDK_ERROR(LX("executeOnPlaybackError").d("error", error)); + sendPlaybackFailedEvent(m_token, ErrorType::MEDIA_ERROR_UNKNOWN, error); + executeStop(); +} + +void AudioPlayer::executeOnPlaybackPaused() { + // TODO: AVS recommends sending this after a recognize event to reduce latency (ACSDK-371). + sendPlaybackPausedEvent(); + changeActivity(PlayerActivity::PAUSED); +} + +void AudioPlayer::executeOnPlaybackResumed() { + sendPlaybackResumedEvent(); + changeActivity(PlayerActivity::PLAYING); +} + +void AudioPlayer::executeOnBufferUnderrun() { + if (PlayerActivity::BUFFER_UNDERRUN == m_currentActivity) { + ACSDK_ERROR(LX("executeOnBufferUnderrunFailed").d("reason", "alreadyInUnderrun")); + return; + } + m_bufferUnderrunTimestamp = std::chrono::steady_clock::now(); + sendPlaybackStutterStartedEvent(); + changeActivity(PlayerActivity::BUFFER_UNDERRUN); +} + +void AudioPlayer::executeOnBufferRefilled() { + sendPlaybackStutterFinishedEvent(); + changeActivity(PlayerActivity::PLAYING); +} + +void AudioPlayer::executePlay(PlayBehavior playBehavior, const AudioItem& audioItem) { + ACSDK_DEBUG9(LX("executePlay").d("playBehavior", playBehavior)); + switch (playBehavior) { + case PlayBehavior::REPLACE_ALL: + executeStop(); + // FALL-THROUGH + case PlayBehavior::REPLACE_ENQUEUED: + m_audioItems.clear(); + // FALL-THROUGH + case PlayBehavior::ENQUEUE: + // Per AVS docs, drop/ignore AudioItems that specify an expectedPreviousToken which does not match the + // current/previous token + if (!audioItem.stream.expectedPreviousToken.empty()) { + auto previousToken = m_audioItems.empty() ? m_token : m_audioItems.back().stream.token; + if (previousToken != audioItem.stream.expectedPreviousToken) { + ACSDK_INFO(LX("executePlayDropped") + .d("reason", "unexpectedPreviousToken") + .d("previous", previousToken) + .d("expected", audioItem.stream.expectedPreviousToken)); + return; + } + } + m_audioItems.push_back(audioItem); + break; + } + + if (m_audioItems.empty()) { + ACSDK_ERROR(LX("executePlayFailed").d("reason", "unhandledPlayBehavior").d("playBehavior", playBehavior)); + return; + } + + if (m_starting || PlayerActivity::PLAYING == m_currentActivity) { + return; + } + + if (!m_focusManager->acquireChannel(CHANNEL_NAME, shared_from_this(), ACTIVITY_ID)) { + ACSDK_ERROR(LX("executePlayFailed") + .d("reason", "CouldNotAcquireChannel")); + sendPlaybackFailedEvent( + m_token, + ErrorType::MEDIA_ERROR_INTERNAL_DEVICE_ERROR, + std::string("Could not acquire ") + CHANNEL_NAME + " for " + ACTIVITY_ID); + return; + } + + m_starting = true; +} + +void AudioPlayer::playNextItem() { + if (m_audioItems.empty()) { + sendPlaybackFailedEvent( + m_token, + ErrorType::MEDIA_ERROR_INTERNAL_DEVICE_ERROR, + "queue is empty"); + ACSDK_ERROR(LX("playNextItemFailed").d("reason", "emptyQueue")); + executeStop(); + return; + } + + auto item = m_audioItems.front(); + m_audioItems.pop_front(); + m_token = item.stream.token; + + if (item.stream.reader) { + if (m_mediaPlayer->setSource(std::move(item.stream.reader)) == MediaPlayerStatus::FAILURE) { + sendPlaybackFailedEvent( + m_token, + ErrorType::MEDIA_ERROR_INTERNAL_DEVICE_ERROR, + "failed to set attachment media source"); + ACSDK_ERROR(LX("playNextItemFailed").d("reason", "setSourceFailed").d("type", "attachment")); + return; + } + } else if (m_mediaPlayer->setSource(item.stream.url) == MediaPlayerStatus::FAILURE) { + sendPlaybackFailedEvent( + m_token, + ErrorType::MEDIA_ERROR_INTERNAL_DEVICE_ERROR, + "failed to set URL media source"); + ACSDK_ERROR(LX("playNextItemFailed").d("reason", "setSourceFailed").d("type", "URL")); + return; + } + + if (item.stream.offset.count() && m_mediaPlayer->setOffset(item.stream.offset) == MediaPlayerStatus::FAILURE) { + sendPlaybackFailedEvent( + m_token, + ErrorType::MEDIA_ERROR_INTERNAL_DEVICE_ERROR, + "failed to set stream offset"); + ACSDK_ERROR(LX("playNextItemFailed").d("reason", "setOffsetFailed")); + return; + } + + if (m_mediaPlayer->play() == MediaPlayerStatus::FAILURE) { + executeOnPlaybackError("playFailed"); + return; + } + if (std::chrono::milliseconds::max() != item.stream.progressReport.delay) { + m_delayTimer.start( + item.stream.progressReport.delay - item.stream.offset, + [this] { + m_executor.submit([this] { sendProgressReportDelayElapsedEvent(); }); + }); + } + if (std::chrono::milliseconds::max() != item.stream.progressReport.interval) { + m_intervalTimer.start( + item.stream.progressReport.interval - item.stream.offset, + item.stream.progressReport.interval, + timing::Timer::PeriodType::ABSOLUTE, + timing::Timer::FOREVER, + [this] { + m_executor.submit([this] { sendProgressReportIntervalElapsedEvent(); }); + }); + } +} + +void AudioPlayer::executeStop() { + ACSDK_DEBUG9(LX("executestop").d("m_currentActivity", m_currentActivity)); + switch (m_currentActivity) { + case PlayerActivity::IDLE: + case PlayerActivity::STOPPED: + if (m_starting) { + break; + } else { + return; + } + // FALL-THROUGH + case PlayerActivity::PLAYING: + case PlayerActivity::PAUSED: + case PlayerActivity::BUFFER_UNDERRUN: + if (m_mediaPlayer->stop() == MediaPlayerStatus::FAILURE) { + executeOnPlaybackError("stopFailed"); + } + break; + default: + break; + } + m_starting = false; + m_delayTimer.stop(); + m_intervalTimer.stop(); + if (m_focus != avsCommon::avs::FocusState::NONE) { + m_focusManager->releaseChannel(CHANNEL_NAME, shared_from_this()); + } + changeActivity(PlayerActivity::STOPPED); + sendPlaybackStoppedEvent(); +} + +void AudioPlayer::executeClearQueue(ClearBehavior clearBehavior) { + switch (clearBehavior) { + case ClearBehavior::CLEAR_ALL: + executeStop(); + // FALL-THROUGH + case ClearBehavior::CLEAR_ENQUEUED: + m_audioItems.clear(); + break; + } + sendPlaybackQueueClearedEvent(); +} + +void AudioPlayer::changeActivity(PlayerActivity activity) { + ACSDK_DEBUG(LX("changeActivity").d("from", m_currentActivity).d("to", activity)); + m_starting = false; + m_currentActivity = activity; + executeProvideState(); +} + +void AudioPlayer::setHandlingCompleted(std::shared_ptr info) { + if (info && info->result) { + info->result->setCompleted(); + } + removeDirective(info); +} + +void AudioPlayer::sendExceptionEncounteredAndReportFailed( + std::shared_ptr info, + const std::string& message, + avsCommon::avs::ExceptionErrorType type) { + m_exceptionEncounteredSender->sendExceptionEncountered(info->directive->getUnparsedDirective(), type, message); + if (info && info->result) { + info->result->setFailed(message); + } + removeDirective(info); +} + +void AudioPlayer::sendEventWithTokenAndOffset(const std::string& eventName) { + rapidjson::Document payload(rapidjson::kObjectType); + payload.AddMember(TOKEN_KEY, m_token, payload.GetAllocator()); + payload.AddMember( + OFFSET_KEY, + std::chrono::duration_cast(getMediaPlayerOffset()).count(), + payload.GetAllocator()); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + if (!payload.Accept(writer)) { + ACSDK_ERROR(LX("sendEventWithTokenAndOffsetFailed").d("reason", "writerRefusedJsonObject")); + return; + } + + auto event = buildJsonEventString(eventName, "", buffer.GetString()); + auto request = std::make_shared(event.second); + m_messageSender->sendMessage(request); +} + +void AudioPlayer::sendPlaybackStartedEvent() { + sendEventWithTokenAndOffset("PlaybackStarted"); +} + +void AudioPlayer::sendPlaybackNearlyFinishedEvent() { + sendEventWithTokenAndOffset("PlaybackNearlyFinished"); +} + +void AudioPlayer::sendProgressReportDelayElapsedEvent() { + sendEventWithTokenAndOffset("ProgressReportDelayElapsed"); +} + +void AudioPlayer::sendProgressReportIntervalElapsedEvent() { + sendEventWithTokenAndOffset("ProgressReportIntervalElapsed"); +} + +void AudioPlayer::sendPlaybackStutterStartedEvent() { + sendEventWithTokenAndOffset("PlaybackStutterStarted"); +} + +void AudioPlayer::sendPlaybackStutterFinishedEvent() { + rapidjson::Document payload(rapidjson::kObjectType); + payload.AddMember(TOKEN_KEY, m_token, payload.GetAllocator()); + payload.AddMember( + OFFSET_KEY, + std::chrono::duration_cast(getMediaPlayerOffset()).count(), + payload.GetAllocator()); + auto stutterDuration = std::chrono::steady_clock::now() - m_bufferUnderrunTimestamp; + payload.AddMember( + STUTTER_DURATION_KEY, + std::chrono::duration_cast(stutterDuration).count(), + payload.GetAllocator()); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + if (!payload.Accept(writer)) { + ACSDK_ERROR(LX("sendPlaybackStutterFinishedEventFailed").d("reason", "writerRefusedJsonObject")); + return; + } + + auto event = buildJsonEventString("PlaybackStutterFinished", "", buffer.GetString()); + auto request = std::make_shared(event.second); + m_messageSender->sendMessage(request); +} + +void AudioPlayer::sendPlaybackFinishedEvent() { + sendEventWithTokenAndOffset("PlaybackFinished"); +} + +void AudioPlayer::sendPlaybackFailedEvent( + const std::string& failingToken, + ErrorType errorType, + const std::string& message) { + rapidjson::Document payload(rapidjson::kObjectType); + payload.AddMember(TOKEN_KEY, failingToken, payload.GetAllocator()); + + rapidjson::Value currentPlaybackState(rapidjson::kObjectType); + currentPlaybackState.AddMember(TOKEN_KEY, m_token, payload.GetAllocator()); + currentPlaybackState.AddMember(OFFSET_KEY, m_mediaPlayer->getOffsetInMilliseconds(), payload.GetAllocator()); + currentPlaybackState.AddMember(ACTIVITY_KEY, playerActivityToString(m_currentActivity), payload.GetAllocator()); + + payload.AddMember("currentPlaybackState", currentPlaybackState, payload.GetAllocator()); + + rapidjson::Value error(rapidjson::kObjectType); + error.AddMember("type", errorTypeToString(errorType), payload.GetAllocator()); + error.AddMember("message", message, payload.GetAllocator()); + + payload.AddMember("error", error, payload.GetAllocator()); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + if (!payload.Accept(writer)) { + ACSDK_ERROR(LX("sendPlaybackStartedEventFailed").d("reason", "writerRefusedJsonObject")); + return; + } + + auto event = buildJsonEventString("PlaybackFailed", "", buffer.GetString()); + auto request = std::make_shared(event.second); + m_messageSender->sendMessage(request); +} + +void AudioPlayer::sendPlaybackStoppedEvent() { + sendEventWithTokenAndOffset("PlaybackStopped"); +} + +void AudioPlayer::sendPlaybackPausedEvent() { + sendEventWithTokenAndOffset("PlaybackPaused"); +} + +void AudioPlayer::sendPlaybackResumedEvent() { + sendEventWithTokenAndOffset("PlaybackResumed"); +} + +void AudioPlayer::sendPlaybackQueueClearedEvent() { + auto event = buildJsonEventString("PlaybackQueueCleared"); + auto request = std::make_shared(event.second); + m_messageSender->sendMessage(request); +} + +void AudioPlayer::sendStreamMetadataExtractedEvent() { + //TODO: Implement/call this once MediaPlayer exports metadata info (ACSDK-414). +} + +std::chrono::milliseconds AudioPlayer::getMediaPlayerOffset() { + auto offset = m_mediaPlayer->getOffsetInMilliseconds(); + if (offset < 0) { + offset = 0; + } + return std::chrono::milliseconds(offset); +} + +} // namespace audioPlayer +} // namespace capabilityAgents +} // namespace alexaClientSDK diff --git a/CapabilityAgents/AudioPlayer/src/CMakeLists.txt b/CapabilityAgents/AudioPlayer/src/CMakeLists.txt new file mode 100644 index 0000000000..4ff111d9d4 --- /dev/null +++ b/CapabilityAgents/AudioPlayer/src/CMakeLists.txt @@ -0,0 +1,8 @@ +add_definitions("-DACSDK_LOG_MODULE=audioplayer") +add_library(AudioPlayer SHARED + AudioPlayer.cpp) +target_include_directories(AudioPlayer PUBLIC + "${AudioPlayer_SOURCE_DIR}/include" + "${AVSCommon_INCLUDE_DIRS}") +target_link_libraries(AudioPlayer + AVSCommon) diff --git a/CapabilityAgents/CMakeLists.txt b/CapabilityAgents/CMakeLists.txt index c677391c85..9d1a59ddf0 100644 --- a/CapabilityAgents/CMakeLists.txt +++ b/CapabilityAgents/CMakeLists.txt @@ -5,5 +5,6 @@ include(../build/BuildDefaults.cmake) add_subdirectory("AIP") add_subdirectory("Alerts") +add_subdirectory("AudioPlayer") add_subdirectory("SpeechSynthesizer") add_subdirectory("System") diff --git a/CapabilityAgents/SpeechSynthesizer/CMakeLists.txt b/CapabilityAgents/SpeechSynthesizer/CMakeLists.txt index 540b560710..07272b5075 100644 --- a/CapabilityAgents/SpeechSynthesizer/CMakeLists.txt +++ b/CapabilityAgents/SpeechSynthesizer/CMakeLists.txt @@ -4,4 +4,4 @@ project(SpeechSynthesizer LANGUAGES CXX) include(../../build/BuildDefaults.cmake) add_subdirectory("src") -add_subdirectory("test") \ No newline at end of file +add_subdirectory("test") diff --git a/CapabilityAgents/SpeechSynthesizer/include/SpeechSynthesizer/SpeechSynthesizer.h b/CapabilityAgents/SpeechSynthesizer/include/SpeechSynthesizer/SpeechSynthesizer.h index 4f99c7e4e1..68cd8df5af 100644 --- a/CapabilityAgents/SpeechSynthesizer/include/SpeechSynthesizer/SpeechSynthesizer.h +++ b/CapabilityAgents/SpeechSynthesizer/include/SpeechSynthesizer/SpeechSynthesizer.h @@ -447,9 +447,6 @@ class SpeechSynthesizer : /// @c SpeakDirectiveInfo instance for the @c AVSDirective currently being handled. std::shared_ptr m_currentInfo; - /// @c Executor which queues up operations from asynchronous API calls. - avsCommon::utils::threading::Executor m_executor; - /// Mutex to serialize access to m_currentState, m_desiredState, and m_waitOnStateChange. std::mutex m_mutex; @@ -471,6 +468,13 @@ class SpeechSynthesizer : /// Serializes access to @c m_speakInfoQueue std::mutex m_speakInfoQueueMutex; + /** + * @c Executor which queues up operations from asynchronous API calls. + * + * @note This declaration needs to come *after* any variables used by the executor thread so that the thread shuts + * down before the variables are destroyed. + */ + avsCommon::utils::threading::Executor m_executor; }; } // namespace speechSynthesizer diff --git a/CapabilityAgents/SpeechSynthesizer/src/CMakeLists.txt b/CapabilityAgents/SpeechSynthesizer/src/CMakeLists.txt index 86481d6162..2e9ff7c8ad 100644 --- a/CapabilityAgents/SpeechSynthesizer/src/CMakeLists.txt +++ b/CapabilityAgents/SpeechSynthesizer/src/CMakeLists.txt @@ -1,3 +1,5 @@ +add_definitions("-DACSDK_LOG_MODULE=speechSynthesizer") + add_library(SpeechSynthesizer SHARED SpeechSynthesizer.cpp) diff --git a/CapabilityAgents/SpeechSynthesizer/src/SpeechSynthesizer.cpp b/CapabilityAgents/SpeechSynthesizer/src/SpeechSynthesizer.cpp index 175fc51390..d148507aa1 100644 --- a/CapabilityAgents/SpeechSynthesizer/src/SpeechSynthesizer.cpp +++ b/CapabilityAgents/SpeechSynthesizer/src/SpeechSynthesizer.cpp @@ -140,6 +140,7 @@ avsCommon::avs::DirectiveHandlerConfiguration SpeechSynthesizer::getConfiguratio } SpeechSynthesizer::~SpeechSynthesizer() { + ACSDK_DEBUG(LX("~SpeechSynthesizer")); m_speechPlayer->setObserver(nullptr); { std::unique_lock lock(m_mutex); @@ -168,35 +169,43 @@ SpeechSynthesizer::~SpeechSynthesizer() { } void SpeechSynthesizer::addObserver(std::shared_ptr observer) { + ACSDK_DEBUG9(LX("addObserver").d("observer", observer.get())); m_executor.submit([this, observer] () { m_observers.insert(observer); }); } void SpeechSynthesizer::removeObserver(std::shared_ptr observer) { + ACSDK_DEBUG9(LX("removeObserver").d("observer", observer.get())); m_executor.submit([this, observer] () { m_observers.erase(observer); }).wait(); } void SpeechSynthesizer::onDeregistered() { + ACSDK_DEBUG9(LX("onDeregistered")); // default no-op } void SpeechSynthesizer::handleDirectiveImmediately(std::shared_ptr directive) { + ACSDK_DEBUG9(LX("handleDirectiveImmediately").d("messageId", directive->getMessageId())); auto info = createDirectiveInfo(directive, nullptr); m_executor.submit([this, info] () { executeHandleImmediately(info); }); } void SpeechSynthesizer::preHandleDirective(std::shared_ptr info) { + ACSDK_DEBUG9(LX("preHandleDirective").d("messageId", info->directive->getMessageId())); m_executor.submit([this, info] () { executePreHandle(info); }); } void SpeechSynthesizer::handleDirective(std::shared_ptr info) { + ACSDK_DEBUG9(LX("handleDirective").d("messageId", info->directive->getMessageId())); m_executor.submit([this, info] () { executeHandle(info); }); } void SpeechSynthesizer::cancelDirective(std::shared_ptr info) { + ACSDK_DEBUG9(LX("cancelDirective").d("messageId", info->directive->getMessageId())); m_executor.submit([this, info] () { executeCancel(info); }); } void SpeechSynthesizer::onFocusChanged(FocusState newFocus) { + ACSDK_DEBUG(LX("onFocusChanged").d("newFocus", newFocus)); std::unique_lock lock(m_mutex); m_currentFocus = newFocus; setDesiredStateLocked(newFocus); @@ -206,8 +215,10 @@ void SpeechSynthesizer::onFocusChanged(FocusState newFocus) { auto messageId = (m_currentInfo && m_currentInfo->directive) ? m_currentInfo->directive->getMessageId() : ""; m_executor.submit([this] () { executeStateChange(); }); // Block until we achieve the desired state. - if (!m_waitOnStateChange.wait_for(lock, STATE_CHANGE_TIMEOUT, [this] () { + if (m_waitOnStateChange.wait_for(lock, STATE_CHANGE_TIMEOUT, [this] () { return m_currentState == m_desiredState; })) { + ACSDK_DEBUG9(LX("onFocusChangedSuccess")); + } else { ACSDK_ERROR(LX("onFocusChangeFailed").d("reason", "stateChangeTimeout") .d("messageId", messageId)); if (m_currentInfo) { @@ -216,32 +227,38 @@ void SpeechSynthesizer::onFocusChanged(FocusState newFocus) { avsCommon::avs::ExceptionErrorType::INTERNAL_ERROR, "stateChangeTimeout"); } - }; + } } void SpeechSynthesizer::provideState(const unsigned int stateRequestToken) { + ACSDK_DEBUG9(LX("provideState").d("token", stateRequestToken)); std::lock_guard lock(m_mutex); auto state = m_currentState; m_executor.submit([this, state, stateRequestToken] () { executeProvideState(state, stateRequestToken); }); } void SpeechSynthesizer::onContextAvailable(const std::string& jsonContext) { + ACSDK_DEBUG9(LX("onContextAvailable").d("context", jsonContext)); // default no-op } void SpeechSynthesizer::onContextFailure(const ContextRequestError error) { + ACSDK_DEBUG9(LX("onContextFailure").d("error", error)); // default no-op } void SpeechSynthesizer::onPlaybackStarted() { + ACSDK_DEBUG9(LX("onPlaybackStarted")); m_executor.submit([this] () { executePlaybackStarted(); }); } void SpeechSynthesizer::onPlaybackFinished() { + ACSDK_DEBUG9(LX("onPlaybackFinished")); m_executor.submit([this] () { executePlaybackFinished(); }); } void SpeechSynthesizer::onPlaybackError(std::string error) { + ACSDK_DEBUG9(LX("onPlaybackError")); m_executor.submit([this, error] () { executePlaybackError(error); }); } @@ -296,6 +313,7 @@ void SpeechSynthesizer::init() { } void SpeechSynthesizer::executeHandleImmediately(std::shared_ptr info) { + ACSDK_DEBUG(LX("executeHandleImmediately").d("messageId", info->directive->getMessageId())); auto speakInfo = validateInfo("executeHandleImmediately", info, false); if (!speakInfo) { ACSDK_ERROR(LX("executeHandleImmediatelyFailed").d("reason", "invalidDirective")); @@ -388,6 +406,7 @@ void SpeechSynthesizer::executeHandleAfterValidation(std::shared_ptr info) { + ACSDK_DEBUG(LX("executePreHandle").d("messageId", info->directive->getMessageId())); auto speakInfo = validateInfo("executePreHandle", info); if (!speakInfo) { ACSDK_ERROR(LX("executePreHandleFailed").d("reason", "invalidDirectiveInfo")); @@ -397,6 +416,7 @@ void SpeechSynthesizer::executePreHandle(std::shared_ptr info) { } void SpeechSynthesizer::executeHandle(std::shared_ptr info) { + ACSDK_DEBUG(LX("executeHandle").d("messageId", info->directive->getMessageId())); auto speakInfo = validateInfo("executeHandle", info); if (!speakInfo) { ACSDK_ERROR(LX("executeHandleFailed").d("reason", "invalidDirectiveInfo")); @@ -406,6 +426,7 @@ void SpeechSynthesizer::executeHandle(std::shared_ptr info) { } void SpeechSynthesizer::executeCancel(std::shared_ptr info) { + ACSDK_DEBUG(LX("executeCancel").d("messageId", info->directive->getMessageId())); auto speakInfo = validateInfo("executeCancel", info); if (!speakInfo) { ACSDK_ERROR(LX("executeCancelFailed").d("reason", "invalidDirectiveInfo")); @@ -442,6 +463,7 @@ void SpeechSynthesizer::executeStateChange() { std::lock_guard lock(m_mutex); newState = m_desiredState; } + ACSDK_DEBUG(LX("executeStateChange").d("newState", newState)); switch (newState) { case SpeechSynthesizerObserver::SpeechSynthesizerState::PLAYING: m_currentInfo->sendPlaybackFinishedMessage = true; @@ -449,6 +471,7 @@ void SpeechSynthesizer::executeStateChange() { startPlaying(); break; case SpeechSynthesizerObserver::SpeechSynthesizerState::FINISHED: + m_currentInfo->sendPlaybackFinishedMessage = false; stopPlaying(); break; } @@ -456,6 +479,7 @@ void SpeechSynthesizer::executeStateChange() { void SpeechSynthesizer::executeProvideState( const SpeechSynthesizerObserver::SpeechSynthesizerState &state, const unsigned int& stateRequestToken) { + ACSDK_DEBUG(LX("executeProvideState").d("stateRequestToken", stateRequestToken)); int64_t offsetInMilliseconds = 0; StateRefreshPolicy refreshPolicy = StateRefreshPolicy::NEVER; std::string speakDirectiveToken; @@ -479,12 +503,13 @@ void SpeechSynthesizer::executeProvideState( auto result = m_contextManager->setState(CONTEXT_MANAGER_SPEECH_STATE, jsonState, refreshPolicy, stateRequestToken); if (result != SetStateResult::SUCCESS) { - ACSDK_ERROR(LX("executeProvideState").d("reason", "contextManagerSetStateFailed"). + ACSDK_ERROR(LX("executeProvideStateFailed").d("reason", "contextManagerSetStateFailed"). d("token", speakDirectiveToken)); } } void SpeechSynthesizer::executePlaybackStarted() { + ACSDK_DEBUG(LX("executePlaybackStarted")); { std::lock_guard lock(m_mutex); setCurrentStateLocked(SpeechSynthesizerObserver::SpeechSynthesizerState::PLAYING); @@ -503,6 +528,7 @@ void SpeechSynthesizer::executePlaybackStarted() { } void SpeechSynthesizer::executePlaybackFinished() { + ACSDK_DEBUG(LX("executePlaybackFinished")); if (!m_currentInfo) { ACSDK_ERROR(LX("executePlaybackFinishedIgnored").d("reason", "nullptrDirectiveInfo")); return; @@ -538,6 +564,7 @@ void SpeechSynthesizer::executePlaybackFinished() { } void SpeechSynthesizer::executePlaybackError(std::string error) { + ACSDK_DEBUG(LX("executePlaybackError").d("error", error)); if (!m_currentInfo) { return; } @@ -589,6 +616,7 @@ std::string SpeechSynthesizer::buildPayload(std::string &token) { } void SpeechSynthesizer::startPlaying() { + ACSDK_DEBUG9(LX("startPlaying")); m_speechPlayer->setSource(std::move(m_currentInfo->attachmentReader)); auto mediaPlayerStatus = m_speechPlayer->play(); switch (mediaPlayerStatus) { @@ -602,6 +630,7 @@ void SpeechSynthesizer::startPlaying() { } void SpeechSynthesizer::stopPlaying() { + ACSDK_DEBUG9(LX("stopPlaying")); auto mediaPlayerStatus = m_speechPlayer->stop(); switch (mediaPlayerStatus) { case MediaPlayerStatus::SUCCESS: @@ -648,6 +677,7 @@ void SpeechSynthesizer::resetCurrentInfo(std::shared_ptr spe } void SpeechSynthesizer::setHandlingCompleted() { + ACSDK_DEBUG9(LX("setHandlingCompleted")); if (m_currentInfo && m_currentInfo->result) { m_currentInfo->result->setCompleted(); } @@ -676,6 +706,7 @@ void SpeechSynthesizer::sendExceptionEncounteredAndReportMissingProperty( } void SpeechSynthesizer::releaseForegroundFocus() { + ACSDK_DEBUG9(LX("releaseForegroundFocus")); { std::lock_guard lock(m_mutex); m_currentFocus = FocusState::NONE; @@ -740,6 +771,7 @@ void SpeechSynthesizer::addToDirectiveQueue(std::shared_ptr m_speakInfoQueue.push_back(speakInfo); executeHandleAfterValidation(speakInfo); } else { + ACSDK_DEBUG9(LX("addToDirectiveQueue").d("queueSize", m_speakInfoQueue.size())); m_speakInfoQueue.push_back(speakInfo); } } diff --git a/CapabilityAgents/SpeechSynthesizer/test/SpeechSynthesizerTest.cpp b/CapabilityAgents/SpeechSynthesizer/test/SpeechSynthesizerTest.cpp index 2b7584757c..6732637e09 100644 --- a/CapabilityAgents/SpeechSynthesizer/test/SpeechSynthesizerTest.cpp +++ b/CapabilityAgents/SpeechSynthesizer/test/SpeechSynthesizerTest.cpp @@ -148,56 +148,7 @@ class MockAttachmentManager : public AttachmentManagerInterface { const std::string & attachmentId, AttachmentReader::Policy policy)); }; -/** - * gmock does not fully support C++11's move only semantics. Replaces the use of unique_ptr in - * @c MediaPlayerInterface with shared_ptr so that methods using unique_ptr can be mocked. - */ -class MediaPlayerMockAdapter : public MediaPlayerInterface { -public: - virtual ~MediaPlayerMockAdapter() = default; - - MediaPlayerStatus setSource( - std::unique_ptr attachmentReader) override; - - MediaPlayerStatus setSource(std::unique_ptr stream, bool repeat) override; - - /** - * Variant of setSource taking a shared_ptr instead of a unique_ptr. - * - * @param attachmentReader The audioAttachment to read. - * @return @c SUCCESS if the the source was set successfully else @c FAILURE. If setSource is called when audio is - * currently playing, the playing audio will be stopped and the source set to the new value. If there is an error - * stopping the player, this will return @c FAILURE. - */ - virtual void setSource( - std::shared_ptr attachmentReader) = 0; - - /** - * Variant of setSource taking a shared_ptr instead of a unique_ptr. - * - * @param stream Object with which to read an incoming audio stream. - * @param repeat Whether the audio stream should be played in a loop until stopped. - * @return @c SUCCESS if the the source was set successfully else @c FAILURE. If setSource is called when audio is - * currently playing, the playing audio will be stopped and the source set to the new value. If there is an error - * stopping the player, this will return @c FAILURE. - */ - virtual void setSource(std::shared_ptr stream, bool repeat) = 0; -}; - -MediaPlayerStatus MediaPlayerMockAdapter::setSource( - std::unique_ptr attachmentReader) { - std::shared_ptr temp(std::move(attachmentReader)); - setSource(temp); - return MediaPlayerStatus::SUCCESS; -} - -MediaPlayerStatus MediaPlayerMockAdapter::setSource(std::unique_ptr stream, bool repeat) { - std::shared_ptr temp(std::move(stream)); - setSource(temp, repeat); - return MediaPlayerStatus::SUCCESS; -} - -class MockMediaPlayer : public MediaPlayerMockAdapter { +class MockMediaPlayer : public MediaPlayerInterface { public: /// Constructor. MockMediaPlayer(); @@ -216,10 +167,21 @@ class MockMediaPlayer : public MediaPlayerMockAdapter { void setObserver( std::shared_ptr playerObserver) /*override*/; - MOCK_METHOD1(setSource, void(std::shared_ptr attachmentReader)); - MOCK_METHOD2(setSource, void(std::shared_ptr stream, bool repeat)); + MOCK_METHOD1(setSource, MediaPlayerStatus(std::shared_ptr attachmentReader)); + MOCK_METHOD2(setSource, MediaPlayerStatus(std::shared_ptr stream, bool repeat)); +#ifdef __clang__ +// Remove warnings when compiling with clang. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Woverloaded-virtual" + MOCK_METHOD1(setSource, MediaPlayerStatus(const std::string& url)); +#pragma clang diagnostic pop +#else + MOCK_METHOD1(setSource, MediaPlayerStatus(const std::string& url)); +#endif MOCK_METHOD0(play, MediaPlayerStatus()); MOCK_METHOD0(stop, MediaPlayerStatus()); + MOCK_METHOD0(pause, MediaPlayerStatus()); + MOCK_METHOD0(resume, MediaPlayerStatus()); MOCK_METHOD0(getOffsetInMilliseconds, int64_t()); /** @@ -315,9 +277,15 @@ std::shared_ptr> MockMediaPlayer::create() { return result; } -MockMediaPlayer::MockMediaPlayer(): m_play{false}, m_stop{false}, m_shutdown{false}, m_wakePlayPromise{}, - m_wakePlayFuture{m_wakePlayPromise.get_future()}, m_wakeStopPromise{}, - m_wakeStopFuture{m_wakeStopPromise.get_future()}, m_playerObserver{nullptr} { +MockMediaPlayer::MockMediaPlayer(): + m_play{false}, + m_stop{false}, + m_shutdown{false}, + m_wakePlayPromise{}, + m_wakePlayFuture{m_wakePlayPromise.get_future()}, + m_wakeStopPromise{}, + m_wakeStopFuture{m_wakeStopPromise.get_future()}, + m_playerObserver{nullptr} { } MockMediaPlayer::~MockMediaPlayer() { @@ -576,7 +544,10 @@ TEST_F(SpeechSynthesizerTest, testCallingHandleImmediately) { EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)).Times(1). WillOnce(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnAcquireChannel)); - EXPECT_CALL(*(m_mockSpeechPlayer.get()), setSource(_)).Times(AtLeast(1)); + EXPECT_CALL( + *(m_mockSpeechPlayer.get()), + setSource(A>())) + .Times(AtLeast(1)); EXPECT_CALL(*(m_mockSpeechPlayer.get()), play()).Times(AtLeast(1)); EXPECT_CALL(*(m_mockSpeechPlayer.get()), getOffsetInMilliseconds()).Times(1).WillOnce(Return(100)); EXPECT_CALL(*(m_mockContextManager.get()), setState( @@ -607,7 +578,10 @@ TEST_F(SpeechSynthesizerTest, testCallingHandle) { EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)).Times(1). WillOnce(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnAcquireChannel)); - EXPECT_CALL(*(m_mockSpeechPlayer.get()), setSource(_)).Times(AtLeast(1)); + EXPECT_CALL( + *(m_mockSpeechPlayer.get()), + setSource(A>())) + .Times(AtLeast(1)); EXPECT_CALL(*(m_mockSpeechPlayer.get()), play()).Times(AtLeast(1)); EXPECT_CALL(*(m_mockSpeechPlayer.get()), getOffsetInMilliseconds()).Times(1).WillOnce(Return(100)); EXPECT_CALL(*(m_mockContextManager.get()), setState( @@ -663,7 +637,10 @@ TEST_F(SpeechSynthesizerTest, testCallingCancelAfterHandle) { EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)).Times(1). WillOnce(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnAcquireChannel)); - EXPECT_CALL(*(m_mockSpeechPlayer.get()), setSource(_)).Times(AtLeast(1)); + EXPECT_CALL( + *(m_mockSpeechPlayer.get()), + setSource(A>())) + .Times(AtLeast(1)); EXPECT_CALL(*(m_mockSpeechPlayer.get()), play()).Times(AtLeast(1)); EXPECT_CALL(*(m_mockSpeechPlayer.get()), getOffsetInMilliseconds()).Times(1).WillOnce(Return(100)); EXPECT_CALL(*(m_mockContextManager.get()), setState( @@ -723,7 +700,10 @@ TEST_F(SpeechSynthesizerTest, testCallingProvideStateWhenPlaying) { EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)).Times(1). WillOnce(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnAcquireChannel)); - EXPECT_CALL(*(m_mockSpeechPlayer.get()), setSource(_)).Times(AtLeast(1)); + EXPECT_CALL( + *(m_mockSpeechPlayer.get()), + setSource(A>())) + .Times(AtLeast(1)); EXPECT_CALL(*(m_mockSpeechPlayer.get()), play()).Times(AtLeast(1)); EXPECT_CALL(*(m_mockSpeechPlayer.get()), getOffsetInMilliseconds()).Times(AtLeast(1)).WillRepeatedly(Return(100)); EXPECT_CALL(*(m_mockContextManager.get()), setState( @@ -773,7 +753,10 @@ TEST_F(SpeechSynthesizerTest, testBargeInWhilePlaying) { EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)).Times(AtLeast(1)). WillRepeatedly(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnAcquireChannel)); - EXPECT_CALL(*(m_mockSpeechPlayer.get()), setSource(_)).Times(AtLeast(1)); + EXPECT_CALL( + *(m_mockSpeechPlayer.get()), + setSource(A>())) + .Times(AtLeast(1)); EXPECT_CALL(*(m_mockSpeechPlayer.get()), play()).Times(AtLeast(1)); EXPECT_CALL(*(m_mockSpeechPlayer.get()), getOffsetInMilliseconds()).Times(1).WillOnce(Return(100)); EXPECT_CALL(*(m_mockContextManager.get()), setState( @@ -803,6 +786,7 @@ TEST_F(SpeechSynthesizerTest, testBargeInWhilePlaying) { ASSERT_TRUE(m_mockSpeechPlayer->waitUntilPlaybackFinished()); ASSERT_TRUE(std::future_status::ready == m_wakeSetStateFuture.wait_for(WAIT_TIMEOUT)); ASSERT_TRUE(std::future_status::ready == m_wakeReleaseChannelFuture.wait_for(WAIT_TIMEOUT)); + ASSERT_TRUE(std::future_status::ready == m_wakeAcquireChannelFuture.wait_for(WAIT_TIMEOUT)); } } // namespace test diff --git a/CapabilityAgents/System/include/System/NotifyingMessageRequest.h b/CapabilityAgents/System/include/System/NotifyingMessageRequest.h new file mode 100644 index 0000000000..f64068762d --- /dev/null +++ b/CapabilityAgents/System/include/System/NotifyingMessageRequest.h @@ -0,0 +1,57 @@ +/* + * NotifyingMessageRequest.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_SYSTEM_INCLUDE_SYSTEM_NOTIFYING_MESSAGE_REQUEST_H_ +#define ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_SYSTEM_INCLUDE_SYSTEM_NOTIFYING_MESSAGE_REQUEST_H_ + +#include + +#include "StateSynchronizer.h" + +namespace alexaClientSDK { +namespace capabilityAgents { +namespace system { + +/** + * This class implements @c MessageRequests to alert observers upon completion of the message. + */ +class NotifyingMessageRequest : public avsCommon::avs::MessageRequest { +public: + /** + * @copyDoc avsCommon::avs::MessageRequest() + * + * Construct a @c MessageRequest while binding it to a @c StateSynchronizer. + * + * @param callback The function to be called when @c onSendCompleted is invoked. + */ + NotifyingMessageRequest(const std::string& jsonContent, std::shared_ptr stateSynchronizer); + + /// @name MessageRequest functions. + /// @{ + void onSendCompleted(avsCommon::avs::MessageRequest::Status status) override; + /// @} + +private: + /// The @c StateSynchronizer to be notified when @c onSendCompleted is called. + std::shared_ptr m_stateSynchronizer; +}; + +} // namespace system +} // namespace capabilityAgents +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_SYSTEM_INCLUDE_SYSTEM_NOTIFYING_MESSAGE_REQUEST_H_ diff --git a/CapabilityAgents/System/include/System/StateSynchronizer.h b/CapabilityAgents/System/include/System/StateSynchronizer.h index 0e756a9e86..724564cdf0 100644 --- a/CapabilityAgents/System/include/System/StateSynchronizer.h +++ b/CapabilityAgents/System/include/System/StateSynchronizer.h @@ -19,12 +19,16 @@ #define ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_SYSTEM_INCLUDE_SYSTEM_STATE_SYNCHRONIZER_H_ #include +#include #include +#include +#include #include #include #include #include +#include namespace alexaClientSDK { namespace capabilityAgents { @@ -35,6 +39,9 @@ class StateSynchronizer : public avsCommon::sdkInterfaces::ContextRequesterInterface, public std::enable_shared_from_this { public: + /// Alias for @c StateSynchronizerObserverInterface for brevity. + using ObserverInterface = avsCommon::sdkInterfaces::StateSynchronizerObserverInterface; + /** * Create an instance of StateSynchronizer. * @@ -58,6 +65,37 @@ class StateSynchronizer : void onContextAvailable(const std::string& jsonContext) override; void onContextFailure(const avsCommon::sdkInterfaces::ContextRequestError error) override; /// @} + + /** + * Add a @c StateSynchronizerObserverInterface to be notified. + * + * @param observer The @c StateSynchronizerObserverInterface to be added + * @note The added observer (if it's not added before) will immediately get @c onStateChanged callback with the + * current state of @c StateSynchronizer. + */ + void addObserver(std::shared_ptr observer); + + /** + * Remove a @c StateSynchronizerObserverInterface from the list of notifiers. + * + * @param observer The @c StateSynchronizerObserverInterface to be removed. + */ + void removeObserver(std::shared_ptr observer); + + /** + * Manage completion of event being sent. + * + * @param messageStatus The status of submitted @c MessageRequest. + */ + void messageSent(avsCommon::avs::MessageRequest::Status messageStatus); + + /** + * Shutdown sequence for the instance. + * + * Performing all cleanup operations to prepare the object for destruction. This function must be called prior to + * destruction to properly clean up the instance. + */ + void shutdown(); private: /** * Constructor. @@ -69,11 +107,29 @@ class StateSynchronizer : std::shared_ptr contextManager, std::shared_ptr messageSender); + /** + * Notify the observers. This function uses @c m_state and therefore should be called when its mutex, + * @c m_stateMutex is locked. + */ + void notifyObserversLocked(); + /// The @c MessageSenderInterface used to send event messages. std::shared_ptr m_messageSender; /// The @c ContextManager used to generate system context for events. std::shared_ptr m_contextManager; + + /// The set of @c StateSynchronizerObserverInterface objects that will be notified upon synchronization. + std::unordered_set> m_observers; + + /// The mutex to synchronize access to @c m_observers. + std::mutex m_observerMutex; + + /// The current state of @c StateSynchronizer. + ObserverInterface::State m_state; + + /// The mutex to synchronize access to @c m_state. + std::mutex m_stateMutex; }; diff --git a/CapabilityAgents/System/include/System/UserInactivityMonitor.h b/CapabilityAgents/System/include/System/UserInactivityMonitor.h new file mode 100644 index 0000000000..709012ca6f --- /dev/null +++ b/CapabilityAgents/System/include/System/UserInactivityMonitor.h @@ -0,0 +1,121 @@ +/* + * UserInactivityMonitor.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_SYSTEM_INCLUDE_SYSTEM_USER_INACTIVITY_MONITOR_H_ +#define ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_SYSTEM_INCLUDE_SYSTEM_USER_INACTIVITY_MONITOR_H_ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace alexaClientSDK { +namespace capabilityAgents { +namespace system { + +/// This class implementes a @c CapabilityAgent that handles the @c SetEndpoint directive. +class UserInactivityMonitor : + public avsCommon::sdkInterfaces::UserActivityNotifierInterface, + public avsCommon::avs::CapabilityAgent { +public: + /** + * Create an instance of @c UserInactivityMonitor. + * + * @param messageSender The @c MessageSenderInterface for sending events. + * @param exceptionEncounteredSender The interface that sends exceptions. + * @param sendPeriod The period of send events in seconds. + * @return @c nullptr if the inputs are not defined, else a new instance of @c UserInactivityMonitor. + */ + static std::shared_ptr create( + std::shared_ptr messageSender, + std::shared_ptr exceptionEncounteredSender, + const std::chrono::milliseconds& sendPeriod = std::chrono::hours(1)); + + /// @name DirectiveHandlerInterface and CapabilityAgent Functions + /// @{ + avsCommon::avs::DirectiveHandlerConfiguration getConfiguration() const override; + void handleDirectiveImmediately(std::shared_ptr directive) override; + void preHandleDirective(std::shared_ptr info) override; + void handleDirective(std::shared_ptr info) override; + void cancelDirective(std::shared_ptr info) override; + /// @} + + /// @name UserActivityNotifierInterface functions + /// @{ + void onUserActive() override; + /// @} +private: + /** + * Constructor. + * + * @param messageSender The @c MessageSenderInterface for sending events. + * @param exceptionEncounteredSender The interface that sends exceptions. + * @param sendPeriod The period of send events in seconds. + */ + UserInactivityMonitor( + std::shared_ptr messageSender, + std::shared_ptr exceptionEncounteredSender, + const std::chrono::milliseconds& sendPeriod); + + /** + * + * Remove the directive (if possible) while invoking callbacks to @c DirectiveHandlerResultInterface. + * + * @param info The @c DirectiveInfo we are trying to remove. + * @param isFailure Boolean flag set to @c true if something went wrong before removing the directive. + * @param report The report that we will pass to @c setFailed in case @c isFailure is @c true. + */ + void removeDirectiveGracefully( + std::shared_ptr info, + bool isFailure = false, + const std::string &report = ""); + + /// Send inactivity report by comparing to the last time active. We will register this function with the timer. + void sendInactivityReport(); + + /// The @c MessageSender interface to send inactivity event. + std::shared_ptr m_messageSender; + + /** + * Time point to keep user inactivity. Access synchronized by @c m_timeMutex, and blocks tracked by @c + * m_recentUpdateBlocked. + */ + std::chrono::time_point m_lastTimeActive; + + /// Time point synchronizer mutex. + std::mutex m_timeMutex; + + /// Flag for notifiying that @c onUserActive was not able to update @c m_lastTimeActive. + std::atomic_bool m_recentUpdateBlocked; + + /// Timer for sending events every hour. + avsCommon::utils::timing::Timer m_eventTimer; + +}; + + +} // namespace system +} // namespace capabilityAgents +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_CAPABILITY_AGENTS_SYSTEM_INCLUDE_SYSTEM_USER_INACTIVITY_MONITOR_H_ diff --git a/CapabilityAgents/System/src/CMakeLists.txt b/CapabilityAgents/System/src/CMakeLists.txt index e904e71e47..e1b870c8c7 100644 --- a/CapabilityAgents/System/src/CMakeLists.txt +++ b/CapabilityAgents/System/src/CMakeLists.txt @@ -1,6 +1,10 @@ +add_definitions("-DACSDK_LOG_MODULE=system") + add_library(AVSSystem SHARED "${CMAKE_CURRENT_LIST_DIR}/EndpointHandler.cpp" - "${CMAKE_CURRENT_LIST_DIR}/StateSynchronizer.cpp") + "${CMAKE_CURRENT_LIST_DIR}/StateSynchronizer.cpp" + "${CMAKE_CURRENT_LIST_DIR}/NotifyingMessageRequest.cpp" + "${CMAKE_CURRENT_LIST_DIR}/UserInactivityMonitor.cpp") target_include_directories(AVSSystem PUBLIC "${AVSSystem_SOURCE_DIR}/include") diff --git a/CapabilityAgents/System/src/NotifyingMessageRequest.cpp b/CapabilityAgents/System/src/NotifyingMessageRequest.cpp new file mode 100644 index 0000000000..bcdd4bfeb7 --- /dev/null +++ b/CapabilityAgents/System/src/NotifyingMessageRequest.cpp @@ -0,0 +1,23 @@ +#include "System/NotifyingMessageRequest.h" + +namespace alexaClientSDK { +namespace capabilityAgents { +namespace system { + +using namespace avsCommon::avs; + +NotifyingMessageRequest::NotifyingMessageRequest( + const std::string& jsonContent, + std::shared_ptr stateSynchronizer) : + MessageRequest(jsonContent), + m_stateSynchronizer{stateSynchronizer} +{ +} + +void NotifyingMessageRequest::onSendCompleted(MessageRequest::Status status) { + m_stateSynchronizer->messageSent(status); +} + +} // namespace system +} // namespace capabilityAgents +} // namespace alexaClientSDK diff --git a/CapabilityAgents/System/src/StateSynchronizer.cpp b/CapabilityAgents/System/src/StateSynchronizer.cpp index df012d7c2a..8bcf4089fd 100644 --- a/CapabilityAgents/System/src/StateSynchronizer.cpp +++ b/CapabilityAgents/System/src/StateSynchronizer.cpp @@ -16,8 +16,8 @@ */ #include "System/StateSynchronizer.h" +#include "System/NotifyingMessageRequest.h" -#include #include #include @@ -58,11 +58,76 @@ std::shared_ptr StateSynchronizer::create( return std::shared_ptr(new StateSynchronizer(contextManager, messageSender)); } +void StateSynchronizer::addObserver(std::shared_ptr observer) { + if (!observer) { + ACSDK_ERROR(LX("addObserverFailed").d("reason", "nullObserver")); + return; + } + std::lock_guard observerLock(m_observerMutex); + if (m_observers.insert(observer).second) { + std::lock_guard stateLock(m_stateMutex); + observer->onStateChanged(m_state); + } else { + ACSDK_DEBUG(LX("addObserverRedundant").d("reason", "observerAlreadyAdded")); + } +} + +void StateSynchronizer::removeObserver(std::shared_ptr observer) { + if (!observer) { + ACSDK_ERROR(LX("removeObserverFailed").d("reason", "nullObserver")); + return; + } + std::lock_guard observerLock(m_observerMutex); + m_observers.erase(observer); +} + +void StateSynchronizer::shutdown() { + std::lock_guard observerLock(m_observerMutex); + m_observers.clear(); +} + +void StateSynchronizer::notifyObserversLocked() { + std::unique_lock observerLock(m_observerMutex); + auto currentObservers = m_observers; + observerLock.unlock(); + for (auto observer : currentObservers) { + observer->onStateChanged(m_state); + } +} + +void StateSynchronizer::messageSent(MessageRequest::Status messageStatus) { + if (MessageRequest::Status::SUCCESS == messageStatus) { + std::lock_guard stateLock(m_stateMutex); + if (ObserverInterface::State::SYNCHRONIZED != m_state) { + m_state = ObserverInterface::State::SYNCHRONIZED; + notifyObserversLocked(); + } + } else { + // If the message send was unsuccessful, send another request to @c ContextManager. + ACSDK_ERROR(LX("messageSendNotSuccessful")); + m_contextManager->getContext(shared_from_this()); + } +} + void StateSynchronizer::onConnectionStatusChanged( const ConnectionStatusObserverInterface::Status status, const ConnectionStatusObserverInterface::ChangedReason reason) { + std::lock_guard stateLock(m_stateMutex); if (ConnectionStatusObserverInterface::Status::CONNECTED == status) { - m_contextManager->getContext(shared_from_this()); + if (ObserverInterface::State::SYNCHRONIZED == m_state) { + ACSDK_ERROR(LX("unexpectedConnectionStatusChange").d("reason", "connectHappenedUnexpectedly")); + } else { + // This is the case when we should send @c SynchronizeState event. + m_contextManager->getContext(shared_from_this()); + } + } else { + if (ObserverInterface::State::NOT_SYNCHRONIZED == m_state) { + ACSDK_INFO(LX("unexpectedConnectionStatusChange").d("reason", "noConnectHappenedUnexpectedly")); + } else { + // This is the case when we should notify observers that the connection is not yet synchronized. + m_state = ObserverInterface::State::NOT_SYNCHRONIZED; + notifyObserversLocked(); + } } } @@ -73,18 +138,22 @@ void StateSynchronizer::onContextAvailable(const std::string& jsonContext) { "", "{}", jsonContext); - m_messageSender->sendMessage(std::make_shared(msgIdAndJsonEvent.second)); + m_messageSender->sendMessage( + std::make_shared(msgIdAndJsonEvent.second, shared_from_this())); } void StateSynchronizer::onContextFailure(const ContextRequestError error) { ACSDK_ERROR(LX("contextRetrievalFailed").d("reason", "contextRequestErrorOccurred").d("error", error)); + ACSDK_DEBUG(LX("retryContextRetrieve").d("reason", "contextRetrievalFailed")); + m_contextManager->getContext(shared_from_this()); } StateSynchronizer::StateSynchronizer( std::shared_ptr contextManager, std::shared_ptr messageSender) : m_messageSender{messageSender}, - m_contextManager{contextManager} + m_contextManager{contextManager}, + m_state{ObserverInterface::State::NOT_SYNCHRONIZED} { } diff --git a/CapabilityAgents/System/src/UserInactivityMonitor.cpp b/CapabilityAgents/System/src/UserInactivityMonitor.cpp new file mode 100644 index 0000000000..32307fc98a --- /dev/null +++ b/CapabilityAgents/System/src/UserInactivityMonitor.cpp @@ -0,0 +1,173 @@ +/* + * UserInactivityMonitor.cpp + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 "System/UserInactivityMonitor.h" + +#include +#include +#include +#include +#include + +#include + +namespace alexaClientSDK { +namespace capabilityAgents { +namespace system { + +using namespace avsCommon::sdkInterfaces; +using namespace avsCommon::avs; +using namespace avsCommon::utils::timing; +using namespace avsCommon::utils::json; +using namespace rapidjson; + +/// String to identify log entries originating from this file. +static const std::string TAG("UserInactivityMonitor"); + +/** + * Create a LogEntry using this file's TAG and the specified event string. + * + * @param The event string for this @c LogEntry. + */ +#define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) + +/// String to identify the AVS namespace of the event we send. +static const std::string USER_INACTIVITY_MONITOR_NAMESPACE = "System"; + +/// String to identify the AVS name of the event we send. +static const std::string INACTIVITY_EVENT_NAME = "UserInactivityReport"; + +/// String to identify the key of the payload associated to the inactivity. +static const std::string INACTIVITY_EVENT_PAYLOAD_KEY = "inactiveTimeInSeconds"; + +/// String to identify the AVS name of the directive we receive. +static const std::string RESET_DIRECTIVE_NAME = "ResetUserInactivity"; + +void UserInactivityMonitor::removeDirectiveGracefully( + std::shared_ptr info, + bool isFailure, + const std::string &report) { + if(info) { + if (info->result) { + if (isFailure) { + info->result->setFailed(report); + } else { + info->result->setCompleted(); + } + if (info->directive) { + CapabilityAgent::removeDirective(info->directive->getMessageId()); + } + } + } +} + +std::shared_ptr UserInactivityMonitor::create( + std::shared_ptr messageSender, + std::shared_ptr exceptionEncounteredSender, + const std::chrono::milliseconds& sendPeriod) { + if (!messageSender) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullMessageSender")); + return nullptr; + } + if (!exceptionEncounteredSender) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullExceptionEncounteredSender")); + return nullptr; + } + return std::shared_ptr( + new UserInactivityMonitor(messageSender, exceptionEncounteredSender, sendPeriod)); +} + +UserInactivityMonitor::UserInactivityMonitor( + std::shared_ptr messageSender, + std::shared_ptr exceptionEncounteredSender, + const std::chrono::milliseconds& sendPeriod) : + CapabilityAgent(USER_INACTIVITY_MONITOR_NAMESPACE, exceptionEncounteredSender), + m_messageSender{messageSender}, + m_lastTimeActive{std::chrono::steady_clock::now()} +{ + m_eventTimer.start( + sendPeriod, + Timer::PeriodType::ABSOLUTE, + Timer::FOREVER, + std::bind(&UserInactivityMonitor::sendInactivityReport, this)); +} + +void UserInactivityMonitor::sendInactivityReport() { + std::chrono::time_point lastTimeActive; + m_recentUpdateBlocked = false; + { + std::lock_guard timeLock(m_timeMutex); + lastTimeActive = m_lastTimeActive; + } + if (m_recentUpdateBlocked) { + std::lock_guard timeLock(m_timeMutex); + m_lastTimeActive = std::chrono::steady_clock::now(); + lastTimeActive = m_lastTimeActive; + } + + Document inactivityPayload(kObjectType); + SizeType payloadKeySize = INACTIVITY_EVENT_PAYLOAD_KEY.length(); + const Pointer::Token payloadKey[] = {{INACTIVITY_EVENT_PAYLOAD_KEY.c_str(),payloadKeySize, kPointerInvalidIndex}}; + auto inactiveTime = std::chrono::duration_cast( + std::chrono::steady_clock::now() - lastTimeActive); + Pointer(payloadKey, 1).Set(inactivityPayload, inactiveTime.count()); + std::string inactivityPayloadString; + jsonUtils::convertToValue(inactivityPayload, &inactivityPayloadString); + + auto inactivityEvent = buildJsonEventString(INACTIVITY_EVENT_NAME, "", inactivityPayloadString); + m_messageSender->sendMessage(std::make_shared(inactivityEvent.second)); +} + +DirectiveHandlerConfiguration UserInactivityMonitor::getConfiguration() const { + return DirectiveHandlerConfiguration{{ + NamespaceAndName{USER_INACTIVITY_MONITOR_NAMESPACE, RESET_DIRECTIVE_NAME}, + BlockingPolicy::NON_BLOCKING}}; +} + +void UserInactivityMonitor::handleDirectiveImmediately(std::shared_ptr directive) { + handleDirective(std::make_shared(directive, nullptr)); +} + +void UserInactivityMonitor::preHandleDirective(std::shared_ptr info) { +} + +void UserInactivityMonitor::handleDirective(std::shared_ptr info) { + onUserActive(); + removeDirectiveGracefully(info); +} + +void UserInactivityMonitor::cancelDirective(std::shared_ptr info) { + if (!info->directive) { + removeDirectiveGracefully(info, true, "nullDirective"); + ACSDK_ERROR(LX("cancelDirectiveFailed").d("reason", "nullDirectiveInDirectiveInfo")); + return; + } + removeDirective(info->directive->getMessageId()); +} + +void UserInactivityMonitor::onUserActive() { + std::unique_lock timeLock(m_timeMutex, std::defer_lock); + if (timeLock.try_lock()) { + m_lastTimeActive = std::chrono::steady_clock::now(); + } else { + m_recentUpdateBlocked = true; + } +} + +} // namespace system +} // namespace capabilityAgents +} // namespace alexaClientSDK diff --git a/CapabilityAgents/System/test/StateSynchronizerTest.cpp b/CapabilityAgents/System/test/StateSynchronizerTest.cpp index b889baacd7..f892fd2fd5 100644 --- a/CapabilityAgents/System/test/StateSynchronizerTest.cpp +++ b/CapabilityAgents/System/test/StateSynchronizerTest.cpp @@ -15,6 +15,8 @@ * permissions and limitations under the License. */ +/// @file StateSynchronizerTest + #include #include @@ -22,6 +24,7 @@ #include #include #include +#include #include "System/StateSynchronizer.h" @@ -73,6 +76,13 @@ static bool checkMessageRequest(std::shared_ptr messageRequest) return payloadNode->value.ObjectEmpty(); } +class TestMessageSender : public MessageSenderInterface { + void sendMessage(std::shared_ptr messageRequest) { + EXPECT_TRUE(checkMessageRequest(messageRequest)); + messageRequest->onSendCompleted(MessageRequest::Status::SUCCESS); + } +}; + /// Test harness for @c StateSynchronizer class. class StateSynchronizerTest : public ::testing::Test { public: @@ -83,12 +93,15 @@ class StateSynchronizerTest : public ::testing::Test { /// Mocked Context Manager. Note that we make it a strict mock to ensure we test the flow completely. std::shared_ptr> m_mockContextManager; /// Mocked Message Sender. Note that we make it a strict mock to ensure we test the flow completely. - std::shared_ptr> m_mockMessageSender; + std::shared_ptr m_mockMessageSender; + /// Mocked State Synchronizer Observer. Note that we make it a strict mock to ensure we test the flow completely. + std::shared_ptr> m_mockStateSynchronizerObserver; }; void StateSynchronizerTest::SetUp() { m_mockContextManager = std::make_shared>(); - m_mockMessageSender = std::make_shared>(); + m_mockMessageSender = std::make_shared(); + m_mockStateSynchronizerObserver = std::make_shared>(); } /** @@ -143,7 +156,29 @@ TEST_F(StateSynchronizerTest, contextReceivedSendsMessage) { auto stateSynchronizer = StateSynchronizer::create(strictMockContextManager, m_mockMessageSender); ASSERT_NE(nullptr, stateSynchronizer); - EXPECT_CALL(*m_mockMessageSender, sendMessage(ResultOf(&checkMessageRequest,Eq(true)))); + stateSynchronizer->onContextAvailable(MOCK_CONTEXT); +} + +/** + * This case tests if @c onContextReceived sends a message request to the message sender interface. + */ +TEST_F(StateSynchronizerTest, contextReceivedSendsMessageAndNotifiesObserver) { + auto stateSynchronizer = StateSynchronizer::create(m_mockContextManager, m_mockMessageSender); + ASSERT_NE(nullptr, stateSynchronizer); + + EXPECT_CALL( + *m_mockStateSynchronizerObserver, + onStateChanged(StateSynchronizerObserverInterface::State::NOT_SYNCHRONIZED)).Times(1); + stateSynchronizer->addObserver(m_mockStateSynchronizerObserver); + + EXPECT_CALL(*m_mockContextManager, getContext(NotNull())); + stateSynchronizer->onConnectionStatusChanged( + ConnectionStatusObserverInterface::Status::CONNECTED, + ConnectionStatusObserverInterface::ChangedReason::ACL_CLIENT_REQUEST); + + EXPECT_CALL( + *m_mockStateSynchronizerObserver, + onStateChanged(StateSynchronizerObserverInterface::State::SYNCHRONIZED)).Times(1); stateSynchronizer->onContextAvailable(MOCK_CONTEXT); } diff --git a/CapabilityAgents/System/test/UserInactivityMonitorTest.cpp b/CapabilityAgents/System/test/UserInactivityMonitorTest.cpp new file mode 100644 index 0000000000..45a831d6d7 --- /dev/null +++ b/CapabilityAgents/System/test/UserInactivityMonitorTest.cpp @@ -0,0 +1,242 @@ +/* + * UserInactivityMonitorTest.cpp + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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. + */ + +/// @file UserInactivityMonitorTest.cpp + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "System/UserInactivityMonitor.h" + +using namespace testing; + +namespace alexaClientSDK { +namespace capabilityAgents { +namespace system { +namespace test { + +using namespace avsCommon::sdkInterfaces::test; +using namespace avsCommon::sdkInterfaces; +using namespace avsCommon::avs; +using namespace avsCommon::utils::json; +using namespace rapidjson; +using ::testing::InSequence; + +/// This is a string for the namespace we are testing for. +static const std::string USER_INACTIVITY_RESET_NAMESPACE = "System"; + +/// This is a string for the correct name the endpointing directive uses. +static const std::string USER_INACTIVITY_RESET_NAME = "ResetUserInactivity"; + +/// This is the string for the message ID used in the directive. +static const std::string USER_INACTIVITY_MESSAGE_ID = "ABC123DEF"; +static const std::string USER_INACTIVITY_PAYLOAD_KEY = "inactiveTimeInSeconds"; +static const std::chrono::milliseconds USER_INACTIVITY_REPORT_PERIOD{20}; + +/// This is the condition variable to be used to control the exit of the test case. +std::condition_variable exitTrigger; + +/** + * Check if message request has errors. + * + * @param messageRequest The message requests to be checked. + * @return @c true if parsing the JSON has error, otherwise @c false. + */ +static bool checkMessageRequest(std::shared_ptr messageRequest) { + rapidjson::Document jsonContent(rapidjson::kObjectType); + if (jsonContent.Parse(messageRequest->getJsonContent()).HasParseError()) { + return false; + } + rapidjson::Value::ConstMemberIterator eventNode; + if (!jsonUtils::findNode(jsonContent, "event", &eventNode)) { + return false; + } + rapidjson::Value::ConstMemberIterator payloadNode; + if (!jsonUtils::findNode(eventNode->value, "payload", &payloadNode)) { + return false; + } + rapidjson::Value::ConstMemberIterator inactivityNode; + if (!jsonUtils::findNode(payloadNode->value, USER_INACTIVITY_PAYLOAD_KEY, &inactivityNode)) { + return false; + } + // The payload should be a long integer. + return inactivityNode->value.IsUint64(); +} + +/** + * Check if message request has errors. + * + * @param messageRequest The message requests to be checked. + * @return @c true if parsing the JSON has error, otherwise @c false. + */ +static bool checkMessageRequestAndReleaseTrigger(std::shared_ptr messageRequest) { + auto returnValue = checkMessageRequest(messageRequest); + exitTrigger.notify_all(); + return returnValue; +} + +/// Test harness for @c UserInactivityMonitor class. +class UserInactivityMonitorTest : public ::testing::Test { +public: + /// Set up the test harness for running a test. + void SetUp() override; + +protected: + /// Mocked Message Sender. Note that we make it a strict mock to ensure we test the flow completely. + std::shared_ptr> m_mockMessageSender; + /// Mocked Exception Encountered Sender. Note that we make it a strict mock to ensure we test the flow completely. + std::shared_ptr> m_mockExceptionEncounteredSender; +}; + +void UserInactivityMonitorTest::SetUp() { + m_mockMessageSender = std::make_shared>(); + m_mockExceptionEncounteredSender = std::make_shared>(); +} + +/** + * This case tests if @c UserInactivityMonitor basic create function works properly + */ +TEST_F(UserInactivityMonitorTest, createSuccessfully) { + std::mutex exitMutex; + std::unique_lock exitLock(exitMutex); + EXPECT_CALL(*m_mockMessageSender, sendMessage(ResultOf(&checkMessageRequestAndReleaseTrigger, Eq(true)))); + + auto userInactivityMonitor = UserInactivityMonitor::create( + m_mockMessageSender, + m_mockExceptionEncounteredSender, + USER_INACTIVITY_REPORT_PERIOD); + ASSERT_NE(nullptr, userInactivityMonitor); + + exitTrigger.wait_for(exitLock, USER_INACTIVITY_REPORT_PERIOD + USER_INACTIVITY_REPORT_PERIOD/2); +} + +/** + * This case tests if possible @c nullptr parameters passed to @c UserInactivityMonitor::create are handled properly. + */ +TEST_F(UserInactivityMonitorTest, createWithError) { + ASSERT_EQ(nullptr, UserInactivityMonitor::create(m_mockMessageSender, nullptr)); + ASSERT_EQ(nullptr, UserInactivityMonitor::create(nullptr, m_mockExceptionEncounteredSender)); + ASSERT_EQ(nullptr, UserInactivityMonitor::create(nullptr, nullptr)); +} + +/** + * This case tests if a directive is handled properly. + */ +TEST_F(UserInactivityMonitorTest, handleDirectiveProperly) { + std::mutex exitMutex; + std::unique_lock exitLock(exitMutex); + EXPECT_CALL(*m_mockMessageSender, sendMessage(ResultOf(&checkMessageRequestAndReleaseTrigger, Eq(true)))); + + auto userInactivityMonitor = UserInactivityMonitor::create( + m_mockMessageSender, + m_mockExceptionEncounteredSender, + USER_INACTIVITY_REPORT_PERIOD); + ASSERT_NE(nullptr, userInactivityMonitor); + + auto directiveSequencer = adsl::DirectiveSequencer::create(m_mockExceptionEncounteredSender); + directiveSequencer->addDirectiveHandler(userInactivityMonitor); + + auto userInactivityDirectiveHeader = std::make_shared( + USER_INACTIVITY_RESET_NAMESPACE, + USER_INACTIVITY_RESET_NAME, + USER_INACTIVITY_MESSAGE_ID); + auto attachmentManager = std::make_shared>(); + std::shared_ptr userInactivityDirective = AVSDirective::create( + "", + userInactivityDirectiveHeader, + "", + attachmentManager, + ""); + + directiveSequencer->onDirective(userInactivityDirective); + exitTrigger.wait_for(exitLock, USER_INACTIVITY_REPORT_PERIOD + USER_INACTIVITY_REPORT_PERIOD/2); +} + +/** + * This case tests if multiple requests are being sent up to AVS. + */ +TEST_F(UserInactivityMonitorTest, sendMultipleReports) { + InSequence s; + std::mutex exitMutex; + std::unique_lock exitLock(exitMutex); + int repetitionCount = 3; + EXPECT_CALL(*m_mockMessageSender, sendMessage(ResultOf(&checkMessageRequest, Eq(true)))) + .Times(repetitionCount - 1); + EXPECT_CALL(*m_mockMessageSender, sendMessage(ResultOf(&checkMessageRequestAndReleaseTrigger, Eq(true)))) + .Times(1); + auto userInactivityMonitor = UserInactivityMonitor::create( + m_mockMessageSender, + m_mockExceptionEncounteredSender, + USER_INACTIVITY_REPORT_PERIOD); + ASSERT_NE(nullptr, userInactivityMonitor); + + exitTrigger.wait_for(exitLock, repetitionCount * USER_INACTIVITY_REPORT_PERIOD + USER_INACTIVITY_REPORT_PERIOD/2); +} + +/** + * This case tests if multiple requests are being sent up to AVS with a reset during the process. + */ +TEST_F(UserInactivityMonitorTest, sendMultipleReportsWithReset) { + InSequence s; + std::mutex exitMutex; + std::unique_lock exitLock(exitMutex); + int repetitionCount = 5; + EXPECT_CALL(*m_mockMessageSender, sendMessage(ResultOf(&checkMessageRequest, Eq(true)))) + .Times(AtLeast(repetitionCount- 1)); + EXPECT_CALL(*m_mockMessageSender, sendMessage(ResultOf(&checkMessageRequestAndReleaseTrigger, Eq(true)))) + .Times(1); + + auto userInactivityMonitor = UserInactivityMonitor::create( + m_mockMessageSender, + m_mockExceptionEncounteredSender, + USER_INACTIVITY_REPORT_PERIOD); + ASSERT_NE(nullptr, userInactivityMonitor); + + auto directiveSequencer = adsl::DirectiveSequencer::create(m_mockExceptionEncounteredSender); + directiveSequencer->addDirectiveHandler(userInactivityMonitor); + + auto userInactivityDirectiveHeader = std::make_shared( + USER_INACTIVITY_RESET_NAMESPACE, + USER_INACTIVITY_RESET_NAME, + USER_INACTIVITY_MESSAGE_ID); + auto attachmentManager = std::make_shared>(); + std::shared_ptr userInactivityDirective = AVSDirective::create( + "", + userInactivityDirectiveHeader, + "", + attachmentManager, + ""); + + std::this_thread::sleep_for(2*USER_INACTIVITY_REPORT_PERIOD + USER_INACTIVITY_REPORT_PERIOD/2); + directiveSequencer->onDirective(userInactivityDirective); + + exitTrigger.wait_for(exitLock, repetitionCount * USER_INACTIVITY_REPORT_PERIOD + USER_INACTIVITY_REPORT_PERIOD/2); +} + +} // namespace test +} // namespace system +} // namespace capabilityAgents +} // namespace alexaClientSDK diff --git a/ContextManager/src/ContextManager.cpp b/ContextManager/src/ContextManager.cpp index 99b840a9f1..6f97facd5f 100644 --- a/ContextManager/src/ContextManager.cpp +++ b/ContextManager/src/ContextManager.cpp @@ -276,7 +276,7 @@ Value ContextManager::buildState(const NamespaceAndName& namespaceAndName, const } state.AddMember(StringRef(HEADER_JSON_KEY), header, allocator); - state.AddMember(StringRef(PAYLOAD_JSON_KEY), payload, allocator); + state.AddMember(StringRef(PAYLOAD_JSON_KEY), Value(payload, allocator), allocator); return state; } diff --git a/Integration/include/.DS_Store b/Integration/include/.DS_Store deleted file mode 100644 index aa3b08e99b..0000000000 Binary files a/Integration/include/.DS_Store and /dev/null differ diff --git a/Integration/include/Integration/.DS_Store b/Integration/include/Integration/.DS_Store deleted file mode 100644 index 5008ddfcf5..0000000000 Binary files a/Integration/include/Integration/.DS_Store and /dev/null differ diff --git a/Integration/include/Integration/TestMediaPlayer.h b/Integration/include/Integration/TestMediaPlayer.h index bcea4e2da9..5dd8a3d132 100644 --- a/Integration/include/Integration/TestMediaPlayer.h +++ b/Integration/include/Integration/TestMediaPlayer.h @@ -46,15 +46,21 @@ class TestMediaPlayer : public avsCommon::utils::mediaPlayer::MediaPlayerInterfa ~TestMediaPlayer(); avsCommon::utils::mediaPlayer::MediaPlayerStatus setSource( - std::unique_ptr attachmentReader) override; + std::shared_ptr attachmentReader) override; avsCommon::utils::mediaPlayer::MediaPlayerStatus setSource( - std::unique_ptr stream, bool repeat) override; + std::shared_ptr stream, bool repeat) override; + + avsCommon::utils::mediaPlayer::MediaPlayerStatus setSource(const std::string& url) override; avsCommon::utils::mediaPlayer::MediaPlayerStatus play() override; avsCommon::utils::mediaPlayer::MediaPlayerStatus stop() override; - + + avsCommon::utils::mediaPlayer::MediaPlayerStatus pause() override; + + avsCommon::utils::mediaPlayer::MediaPlayerStatus resume() override; + int64_t getOffsetInMilliseconds() override; void setObserver( @@ -68,9 +74,9 @@ class TestMediaPlayer : public avsCommon::utils::mediaPlayer::MediaPlayerInterfa /// The AttachmentReader to read audioData from. std::shared_ptr m_attachmentReader; /// Timer to wait to send onPlaybackFinished to the observer. - std::unique_ptr m_timer; + std::shared_ptr m_timer; // istream for Alerts. - std::unique_ptr m_istream; + std::shared_ptr m_istream; }; } // namespace test } // namespace integration diff --git a/Integration/include/Integration/TestMessageSender.h b/Integration/include/Integration/TestMessageSender.h index 8bcc7b5513..63077c409f 100644 --- a/Integration/include/Integration/TestMessageSender.h +++ b/Integration/include/Integration/TestMessageSender.h @@ -23,6 +23,8 @@ #include #include +#include + #include "AVSCommon/SDKInterfaces/MessageSenderInterface.h" #include "AVSCommon/SDKInterfaces/MessageObserverInterface.h" diff --git a/Integration/inputs/recognize_flashbriefing_test.wav b/Integration/inputs/recognize_flashbriefing_test.wav new file mode 100644 index 0000000000..2719b0e9b2 Binary files /dev/null and b/Integration/inputs/recognize_flashbriefing_test.wav differ diff --git a/Integration/inputs/recognize_sing_song_test.wav b/Integration/inputs/recognize_sing_song_test.wav new file mode 100644 index 0000000000..39505d8a0d Binary files /dev/null and b/Integration/inputs/recognize_sing_song_test.wav differ diff --git a/Integration/src/.DS_Store b/Integration/src/.DS_Store deleted file mode 100644 index 5008ddfcf5..0000000000 Binary files a/Integration/src/.DS_Store and /dev/null differ diff --git a/Integration/src/CMakeLists.txt b/Integration/src/CMakeLists.txt index 30bf1cd339..e87f9f635c 100644 --- a/Integration/src/CMakeLists.txt +++ b/Integration/src/CMakeLists.txt @@ -1,3 +1,4 @@ +add_definitions("-DACSDK_LOG_MODULE=integration") file(GLOB_RECURSE INTEGRATION_SRC "*.cpp") add_library(Integration SHARED "${INTEGRATION_SRC}") target_include_directories(Integration PUBLIC "${ACL_SOURCE_DIR}/include") @@ -7,6 +8,7 @@ target_include_directories(Integration PUBLIC "${CapabilityAgents_SOURCE_DIR}/in target_include_directories(Integration PUBLIC "${AIP_SOURCE_DIR}/include") target_include_directories(Integration PUBLIC "${SpeechSynthesizer_SOURCE_DIR}/include") target_include_directories(Integration PUBLIC "${Alerts_SOURCE_DIR}/include") +target_include_directories(Integration PUBLIC "${AudioPlayer_SOURCE_DIR}/include") target_include_directories(Integration PUBLIC "${AVSSystem_SOURCE_DIR}/include") target_include_directories(Integration PUBLIC "${KITTAI_SOURCE_DIR}/include") diff --git a/Integration/src/TestMediaPlayer.cpp b/Integration/src/TestMediaPlayer.cpp index 2dad99ff04..e92f8d8978 100644 --- a/Integration/src/TestMediaPlayer.cpp +++ b/Integration/src/TestMediaPlayer.cpp @@ -35,17 +35,21 @@ TestMediaPlayer::~TestMediaPlayer() { } avsCommon::utils::mediaPlayer::MediaPlayerStatus TestMediaPlayer::setSource( - std::unique_ptr attachmentReader) { + std::shared_ptr attachmentReader) { m_attachmentReader = std::move(attachmentReader); return avsCommon::utils::mediaPlayer::MediaPlayerStatus::SUCCESS; } avsCommon::utils::mediaPlayer::MediaPlayerStatus TestMediaPlayer::setSource( - std::unique_ptr stream, bool repeat) { - m_istream = std::move(stream); + std::shared_ptr stream, bool repeat) { + m_istream = stream; return avsCommon::utils::mediaPlayer::MediaPlayerStatus::PENDING; } +avsCommon::utils::mediaPlayer::MediaPlayerStatus TestMediaPlayer::setSource(const std::string& url) { + return avsCommon::utils::mediaPlayer::MediaPlayerStatus::SUCCESS; +} + avsCommon::utils::mediaPlayer::MediaPlayerStatus TestMediaPlayer::play() { if (m_observer) { m_observer->onPlaybackStarted(); @@ -78,6 +82,16 @@ avsCommon::utils::mediaPlayer::MediaPlayerStatus TestMediaPlayer::stop() { } } +// TODO Add implementation +avsCommon::utils::mediaPlayer::MediaPlayerStatus TestMediaPlayer::pause() { + return avsCommon::utils::mediaPlayer::MediaPlayerStatus::SUCCESS; +} + +// TODO Add implementation +avsCommon::utils::mediaPlayer::MediaPlayerStatus TestMediaPlayer::resume() { + return avsCommon::utils::mediaPlayer::MediaPlayerStatus::SUCCESS; +} + int64_t TestMediaPlayer::getOffsetInMilliseconds() { return 0; } diff --git a/Integration/src/TestMessageSender.cpp b/Integration/src/TestMessageSender.cpp index 1217f968d7..e0125eccb6 100644 --- a/Integration/src/TestMessageSender.cpp +++ b/Integration/src/TestMessageSender.cpp @@ -34,6 +34,9 @@ TestMessageSender::TestMessageSender( std::shared_ptr messageObserver) { m_connectionManager = acl::AVSConnectionManager::create(messageRouter, isEnabled, { connectionStatusObserver }, { messageObserver }); + // TODO: ACSDK-421: Remove the callback when m_avsConnection manager is no longer an observer to + // StateSynchronizer. + m_connectionManager->onStateChanged(StateSynchronizerObserverInterface::State::SYNCHRONIZED); } void TestMessageSender::sendMessage(std::shared_ptr request) { diff --git a/Integration/test/.DS_Store b/Integration/test/.DS_Store deleted file mode 100644 index 5008ddfcf5..0000000000 Binary files a/Integration/test/.DS_Store and /dev/null differ diff --git a/Integration/test/AlertsIntegrationTest.cpp b/Integration/test/AlertsIntegrationTest.cpp index 6a384fe6e9..5c74accadb 100644 --- a/Integration/test/AlertsIntegrationTest.cpp +++ b/Integration/test/AlertsIntegrationTest.cpp @@ -58,6 +58,7 @@ #include "Integration/TestSpeechSynthesizerObserver.h" #include "SpeechSynthesizer/SpeechSynthesizer.h" #include "System/StateSynchronizer.h" +#include "System/UserInactivityMonitor.h" #ifdef GSTREAMER_MEDIA_PLAYER #include "MediaPlayer/MediaPlayer.h" @@ -137,7 +138,7 @@ static const std::string CONTENT_ACTIVITY_ID = "Content"; /// Sample alerts activity id. static const std::string ALERTS_ACTIVITY_ID = "Alerts"; // This Integer to be used to specify a timeout in seconds. -static const std::chrono::seconds WAIT_FOR_TIMEOUT_DURATION(15); +static const std::chrono::seconds WAIT_FOR_TIMEOUT_DURATION(20); /// The compatible encoding for AIP. static const avsCommon::utils::AudioFormat::Encoding COMPATIBLE_ENCODING = avsCommon::utils::AudioFormat::Encoding::LPCM; @@ -181,6 +182,34 @@ static const std::string TAG("AlertsIntegrationTest"); std::string configPath; std::string inputPath; +/// A test observer to wait for state synchronizer. +class TestStateSynchronizerObserver : public StateSynchronizerObserverInterface { +public: + TestStateSynchronizerObserver() : m_state{State::NOT_SYNCHRONIZED} {} + void onStateChanged(State newState) override { + std::lock_guard lock(m_mutex); + m_state = newState; + m_conditionVariable.notify_all(); + } + + /** + * Wait the state sychronizer to notify us of a state change to the specified state. + * + * @param state The state to wait for. + * @param timeout The amount of time to wait for the requested state change. + * @return @c true if the state change occured within the specified timeout, else @c false. + */ + bool waitForState(State state, std::chrono::milliseconds timeout) { + std::unique_lock lock(m_mutex); + return m_conditionVariable.wait_for(lock, timeout, [this, state] () { return state == m_state; }); + } + +private: + State m_state; + std::mutex m_mutex; + std::condition_variable m_conditionVariable; +}; + /// A test observer that mocks out the ChannelObserverInterface##onFocusChanged() call. class TestClient : public ChannelObserverInterface { public: @@ -287,7 +316,7 @@ class AlertsTest : public ::testing::Test { m_focusManager = std::make_shared(); m_testContentClient = std::make_shared(); ASSERT_TRUE(m_focusManager->acquireChannel(FocusManager::CONTENT_CHANNEL_NAME, m_testContentClient, CONTENT_ACTIVITY_ID)); - bool focusChanged; + bool focusChanged = false; ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::FOREGROUND); ASSERT_TRUE(focusChanged); @@ -330,13 +359,17 @@ class AlertsTest : public ::testing::Test { m_holdToTalkButton = std::make_shared(); - m_AudioInputProcessor = AudioInputProcessor::create( + m_userInactivityMonitor = UserInactivityMonitor::create( + m_avsConnectionManager, + m_exceptionEncounteredSender); + m_AudioInputProcessor = AudioInputProcessor::create( m_directiveSequencer, m_avsConnectionManager, m_contextManager, m_focusManager, m_dialogUXStateAggregator, - m_exceptionEncounteredSender + m_exceptionEncounteredSender, + m_userInactivityMonitor ); ASSERT_NE (nullptr, m_AudioInputProcessor); m_AudioInputProcessor->addObserver(m_dialogUXStateAggregator); @@ -373,19 +406,28 @@ class AlertsTest : public ::testing::Test { m_alertRenderer, m_alertStorage, m_alertObserver); + ASSERT_NE(m_alertsAgent, nullptr); m_alertsAgent->onLocalStop(); m_alertsAgent->removeAllAlerts(); m_directiveSequencer->addDirectiveHandler(m_alertsAgent); m_avsConnectionManager->addConnectionStatusObserver(m_alertsAgent); + // TODO: ACSDK-421: Revert this to use m_connectionManager rather than m_messageRouter. m_stateSynchronizer = StateSynchronizer::create( m_contextManager, - m_avsConnectionManager); + m_messageRouter); ASSERT_NE(nullptr, m_stateSynchronizer); + + auto stateSynchronizerObserver = std::make_shared(); + m_stateSynchronizer->addObserver(stateSynchronizerObserver); + m_avsConnectionManager->addConnectionStatusObserver(m_stateSynchronizer); connect(); + ASSERT_TRUE(stateSynchronizerObserver->waitForState( + StateSynchronizerObserverInterface::State::SYNCHRONIZED, + WAIT_FOR_TIMEOUT_DURATION)); m_alertsAgent->enableSendEvents(); } @@ -488,7 +530,7 @@ class AlertsTest : public ::testing::Test { ASSERT_TRUE(m_holdToTalkButton->startRecognizing(m_AudioInputProcessor, m_HoldToTalkAudioProvider)); // Put audio onto the SDS saying "Tell me a joke". - bool error; + bool error = false; std::string file = inputPath + audioFile; std::vector audioData = readAudioFromFile(file, &error); ASSERT_FALSE(error); @@ -527,6 +569,7 @@ class AlertsTest : public ::testing::Test { std::unique_ptr m_AudioBufferWriter; std::shared_ptr m_AudioBuffer; std::shared_ptr m_AudioInputProcessor; + std::shared_ptr m_userInactivityMonitor; FocusState m_focusState; std::mutex m_mutex; @@ -550,9 +593,6 @@ class AlertsTest : public ::testing::Test { * Set a 5 second timer, ensure it goes off, then use local stop and make sure the timer is stopped. */ TEST_F(AlertsTest, handleOneTimerWithLocalStop) { - TestMessageSender::SendParams sendSyncParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(checkSentEventName(sendSyncParams, NAME_SYNC_STATE)); - // Write audio to SDS saying "Set a timer for 5 seconds" sendAudioFileAsRecognize(RECOGNIZE_TIMER_AUDIO_FILE_NAME); TestMessageSender::SendParams sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); @@ -575,15 +615,14 @@ TEST_F(AlertsTest, handleOneTimerWithLocalStop) { ASSERT_EQ(m_alertObserver->waitForNext(WAIT_FOR_TIMEOUT_DURATION).state, AlertObserverInterface::State::STARTED); // The test channel client has been notified the content channel has been backgrounded. - bool focusChanged; + bool focusChanged = false; ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::BACKGROUND); ASSERT_TRUE(focusChanged); // Locally stop the alarm. m_alertsAgent->onLocalStop(); - // This step removed as a workaround for our gstreamer implementation behaving incorrectly. - //ASSERT_EQ(m_alertObserver->waitForNext(WAIT_FOR_TIMEOUT_DURATION).state, AlertObserverInterface::State::STOPPED); + ASSERT_EQ(m_alertObserver->waitForNext(WAIT_FOR_TIMEOUT_DURATION).state, AlertObserverInterface::State::STOPPED); // AlertStopped Event is sent. sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); @@ -600,16 +639,13 @@ TEST_F(AlertsTest, handleOneTimerWithLocalStop) { * Set two second timer, ensure they go off, then stop both timers. */ TEST_F(AlertsTest, handleMultipleTimersWithLocalStop) { - TestMessageSender::SendParams sendSyncParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(checkSentEventName(sendSyncParams, NAME_SYNC_STATE)); - // Write audio to SDS saying "Set a timer for 15 seconds". sendAudioFileAsRecognize(RECOGNIZE_VERY_LONG_TIMER_AUDIO_FILE_NAME); TestMessageSender::SendParams sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_TRUE(checkSentEventName(sendParams, NAME_RECOGNIZE)); // The test channel client has been notified the content channel has been backgrounded. - bool focusChanged; + bool focusChanged = false; ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::BACKGROUND); ASSERT_TRUE(focusChanged); @@ -672,7 +708,7 @@ TEST_F(AlertsTest, handleMultipleTimersWithLocalStop) { ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::BACKGROUND); ASSERT_TRUE(focusChanged); - std::this_thread::sleep_for(std::chrono::milliseconds(2000)); + std::this_thread::sleep_for(std::chrono::milliseconds(600)); // Locally stop the second alarm. m_alertsAgent->onLocalStop(); @@ -693,9 +729,6 @@ TEST_F(AlertsTest, handleMultipleTimersWithLocalStop) { * stopped. */ TEST_F(AlertsTest, stealChannelFromActiveAlert) { - TestMessageSender::SendParams sendSyncParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(checkSentEventName(sendSyncParams, NAME_SYNC_STATE)); - // Write audio to SDS saying "Set a timer for 5 seconds" sendAudioFileAsRecognize(RECOGNIZE_TIMER_AUDIO_FILE_NAME); TestMessageSender::SendParams sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); @@ -717,7 +750,7 @@ TEST_F(AlertsTest, stealChannelFromActiveAlert) { ASSERT_EQ(m_alertObserver->waitForNext(WAIT_FOR_TIMEOUT_DURATION).state, AlertObserverInterface::State::STARTED); // The test channel client has been notified the content channel has been backgrounded. - bool focusChanged; + bool focusChanged = false; ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::BACKGROUND); ASSERT_TRUE(focusChanged); @@ -728,8 +761,7 @@ TEST_F(AlertsTest, stealChannelFromActiveAlert) { sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_TRUE(checkSentEventName(sendParams, NAME_ALERT_STOPPED)); - // This step removed as a workaround for our gstreamer implementation behaving incorrectly. - //ASSERT_EQ(m_alertObserver->waitForNext(WAIT_FOR_TIMEOUT_DURATION).state, AlertObserverInterface::State::STOPPED); + ASSERT_EQ(m_alertObserver->waitForNext(WAIT_FOR_TIMEOUT_DURATION).state, AlertObserverInterface::State::STOPPED); // Release the alerts channel. m_focusManager->releaseChannel(FocusManager::ALERTS_CHANNEL_NAME, m_testDialogClient); @@ -746,9 +778,6 @@ TEST_F(AlertsTest, stealChannelFromActiveAlert) { * Locally stop the alert and ensure AlertStopped is sent. */ TEST_F(AlertsTest, DisconnectAndReconnectBeforeLocalStop) { - TestMessageSender::SendParams sendSyncParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(checkSentEventName(sendSyncParams, NAME_SYNC_STATE)); - // Write audio to SDS saying "Set a timer for 5 seconds" sendAudioFileAsRecognize(RECOGNIZE_TIMER_AUDIO_FILE_NAME); TestMessageSender::SendParams sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); @@ -770,15 +799,12 @@ TEST_F(AlertsTest, DisconnectAndReconnectBeforeLocalStop) { std::this_thread::sleep_for(std::chrono::milliseconds(6000)); // The test channel client has been notified the content channel has been backgrounded. - bool focusChanged; + bool focusChanged = false; ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::BACKGROUND); ASSERT_TRUE(focusChanged); connect(); - sendSyncParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(checkSentEventName(sendSyncParams, NAME_SYNC_STATE)); - // Locally stop the alarm. m_alertsAgent->onLocalStop(); @@ -801,9 +827,6 @@ TEST_F(AlertsTest, DisconnectAndReconnectBeforeLocalStop) { * and ensure AlertStopped is sent. */ TEST_F(AlertsTest, DisconnectAndReconnect) { - TestMessageSender::SendParams sendSyncParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(checkSentEventName(sendSyncParams, NAME_SYNC_STATE)); - // Write audio to SDS saying "Set a timer for 5 seconds" sendAudioFileAsRecognize(RECOGNIZE_TIMER_AUDIO_FILE_NAME); TestMessageSender::SendParams sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); @@ -824,18 +847,16 @@ TEST_F(AlertsTest, DisconnectAndReconnect) { std::this_thread::sleep_for(std::chrono::milliseconds(6000)); // The test channel client has been notified the content channel has been backgrounded. - bool focusChanged; - ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::BACKGROUND); - ASSERT_TRUE(focusChanged); + bool focusChanged = false; + + EXPECT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::BACKGROUND); + EXPECT_TRUE(focusChanged); // Locally stop the alarm. m_alertsAgent->onLocalStop(); connect(); - sendSyncParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(checkSentEventName(sendSyncParams, NAME_SYNC_STATE)); - //AlertStopped Event is sent. sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_FALSE(checkSentEventName(sendParams, NAME_ALERT_STOPPED)); @@ -852,14 +873,11 @@ TEST_F(AlertsTest, DisconnectAndReconnect) { * events are sent for it. */ TEST_F(AlertsTest, RemoveAllAlertsBeforeAlertIsActive) { - TestMessageSender::SendParams sendSyncParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(checkSentEventName(sendSyncParams, NAME_SYNC_STATE)); - // Write audio to SDS saying "Set a timer for 5 seconds" sendAudioFileAsRecognize(RECOGNIZE_TIMER_AUDIO_FILE_NAME); TestMessageSender::SendParams sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_TRUE(checkSentEventName(sendParams, NAME_RECOGNIZE)); - bool focusChanged; + bool focusChanged = false; ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::BACKGROUND); ASSERT_TRUE(focusChanged); ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::FOREGROUND); @@ -899,9 +917,6 @@ TEST_F(AlertsTest, RemoveAllAlertsBeforeAlertIsActive) { * and the DeleteAlertSucceeded event is sent. */ TEST_F(AlertsTest, cancelAlertBeforeItIsActive) { - TestMessageSender::SendParams sendSyncParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(checkSentEventName(sendSyncParams, NAME_SYNC_STATE)); - // Write audio to SDS saying "Set a timer for 10 seconds" sendAudioFileAsRecognize(RECOGNIZE_LONG_TIMER_AUDIO_FILE_NAME); TestMessageSender::SendParams sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); @@ -927,10 +942,6 @@ TEST_F(AlertsTest, cancelAlertBeforeItIsActive) { sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_TRUE(checkSentEventName(sendParams, NAME_DELETE_ALERT_SUCCEEDED)); - bool focusChanged; - ASSERT_NE(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::BACKGROUND); - ASSERT_TRUE(focusChanged); - // Speech is handled. sendStartedParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_TRUE(checkSentEventName(sendStartedParams, NAME_SPEECH_STARTED)); @@ -938,10 +949,13 @@ TEST_F(AlertsTest, cancelAlertBeforeItIsActive) { ASSERT_TRUE(checkSentEventName(sendFinishedParams, NAME_SPEECH_FINISHED)); // Low priority Test client gets back permission to the test channel. + bool focusChanged = false; ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::FOREGROUND); - ASSERT_TRUE(focusChanged); - // AlertStarted Event is sent. + m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged); + ASSERT_FALSE(focusChanged); + + // AlertStarted Event is not sent. sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_FALSE(checkSentEventName(sendParams, NAME_ALERT_STARTED)); } @@ -953,8 +967,6 @@ TEST_F(AlertsTest, cancelAlertBeforeItIsActive) { */ TEST_F(AlertsTest, RemoveStorageBeforeAlarmIsSet) { m_alertStorage->close(); - TestMessageSender::SendParams sendSyncParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(checkSentEventName(sendSyncParams, NAME_SYNC_STATE)); // Write audio to SDS saying "Set a timer for 5 seconds" sendAudioFileAsRecognize(RECOGNIZE_LONG_TIMER_AUDIO_FILE_NAME); @@ -967,7 +979,7 @@ TEST_F(AlertsTest, RemoveStorageBeforeAlarmIsSet) { TestMessageSender::SendParams sendFinishedParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_TRUE(checkSentEventName(sendFinishedParams, NAME_SPEECH_FINISHED)); - bool focusChanged; + bool focusChanged = false; ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::FOREGROUND); ASSERT_TRUE(focusChanged); @@ -986,8 +998,6 @@ TEST_F(AlertsTest, RemoveStorageBeforeAlarmIsSet) { ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::FOREGROUND); ASSERT_TRUE(focusChanged); - m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged); - ASSERT_FALSE(focusChanged); } /** @@ -997,9 +1007,6 @@ TEST_F(AlertsTest, RemoveStorageBeforeAlarmIsSet) { * the background. When the speak is complete, the alert is forgrounded and can be locally stopped. */ TEST_F(AlertsTest, UserShortUnrelatedBargeInOnActiveTimer) { - TestMessageSender::SendParams sendSyncParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(checkSentEventName(sendSyncParams, NAME_SYNC_STATE)); - // Write audio to SDS saying "Set a timer for 5 seconds" sendAudioFileAsRecognize(RECOGNIZE_TIMER_AUDIO_FILE_NAME); TestMessageSender::SendParams sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); @@ -1020,7 +1027,7 @@ TEST_F(AlertsTest, UserShortUnrelatedBargeInOnActiveTimer) { ASSERT_TRUE(checkSentEventName(sendParams, NAME_ALERT_STARTED)); // The test channel client has been notified the content channel has been backgrounded. - bool focusChanged; + bool focusChanged = false; ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::BACKGROUND); ASSERT_TRUE(focusChanged); @@ -1042,19 +1049,19 @@ TEST_F(AlertsTest, UserShortUnrelatedBargeInOnActiveTimer) { sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_TRUE(checkSentEventName(sendParams, NAME_ALERT_ENTERED_FOREGROUND)); + std::this_thread::sleep_for(std::chrono::milliseconds(600)); + // Locally stop the alarm. m_alertsAgent->onLocalStop(); // AlertStopped Event is sent. sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(checkSentEventName(sendParams, NAME_ALERT_STOPPED)); + EXPECT_TRUE(checkSentEventName(sendParams, NAME_ALERT_STOPPED)); // Low priority Test client gets back permission to the test channel EXPECT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::FOREGROUND); EXPECT_TRUE(focusChanged); - m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged); - ASSERT_FALSE(focusChanged); } /** @@ -1064,9 +1071,6 @@ TEST_F(AlertsTest, UserShortUnrelatedBargeInOnActiveTimer) { * the background. When all the speaks are complete, the alert is forgrounded and can be locally stopped. */ TEST_F(AlertsTest, UserLongUnrelatedBargeInOnActiveTimer) { - TestMessageSender::SendParams sendSyncParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(checkSentEventName(sendSyncParams, NAME_SYNC_STATE)); - // Write audio to SDS saying "Set a timer for 5 seconds" sendAudioFileAsRecognize(RECOGNIZE_TIMER_AUDIO_FILE_NAME); TestMessageSender::SendParams sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); @@ -1090,7 +1094,7 @@ TEST_F(AlertsTest, UserLongUnrelatedBargeInOnActiveTimer) { ASSERT_TRUE(checkSentEventName(sendParams, NAME_ALERT_STARTED)); // The test channel client has been notified the content channel has been backgrounded. - bool focusChanged; + bool focusChanged = false; ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::BACKGROUND); ASSERT_TRUE(focusChanged); @@ -1108,19 +1112,16 @@ TEST_F(AlertsTest, UserLongUnrelatedBargeInOnActiveTimer) { ASSERT_TRUE(checkSentEventName(sendParams, NAME_ALERT_ENTERED_BACKGROUND)); } - // sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - // ASSERT_TRUE(checkSentEventName(sendParams, NAME_RECOGNIZE)); - - // sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - // ASSERT_TRUE(checkSentEventName(sendParams, NAME_ALERT_ENTERED_BACKGROUND)); - // Speech is handled. sendStartedParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); + if (getSentEventName(sendParams) == NAME_ALERT_ENTERED_FOREGROUND) { + sendStartedParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); + } ASSERT_TRUE(checkSentEventName(sendStartedParams, NAME_SPEECH_STARTED)); sendFinishedParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_TRUE(checkSentEventName(sendFinishedParams, NAME_SPEECH_FINISHED)); - // For each speak directive that results from "what's up", the alert losses anf gains foreground. + // For each speak directive that results from "what's up", the alert losses and gains foreground. for (int i = 0; i <4; i++) { sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_TRUE(checkSentEventName(sendParams, NAME_ALERT_ENTERED_FOREGROUND)); @@ -1133,15 +1134,11 @@ TEST_F(AlertsTest, UserLongUnrelatedBargeInOnActiveTimer) { ASSERT_TRUE(checkSentEventName(sendFinishedParams, NAME_SPEECH_FINISHED)); } + std::this_thread::sleep_for(std::chrono::milliseconds(600)); + // Locally stop the alarm. m_alertsAgent->onLocalStop(); - // These steps removed as a workaround for our gstreamer implementation behaving incorrectly. - // AlertStopped Event is sent. - // sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - //ASSERT_TRUE(checkSentEventName(sendParams, NAME_ALERT_STOPPED)); - - sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); if (getSentEventName(sendParams) == NAME_ALERT_ENTERED_FOREGROUND) { // AlertStopped Event is sent @@ -1156,8 +1153,6 @@ TEST_F(AlertsTest, UserLongUnrelatedBargeInOnActiveTimer) { EXPECT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::FOREGROUND); EXPECT_TRUE(focusChanged); - m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged); - ASSERT_FALSE(focusChanged); } /** @@ -1168,9 +1163,6 @@ TEST_F(AlertsTest, UserLongUnrelatedBargeInOnActiveTimer) { * before locally stopping it. */ TEST_F(AlertsTest, UserSpeakingWhenAlertShouldBeActive) { - TestMessageSender::SendParams sendSyncParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(checkSentEventName(sendSyncParams, NAME_SYNC_STATE)); - // Write audio to SDS saying "Set a timer for 10 seconds" sendAudioFileAsRecognize(RECOGNIZE_LONG_TIMER_AUDIO_FILE_NAME); TestMessageSender::SendParams sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); @@ -1186,49 +1178,58 @@ TEST_F(AlertsTest, UserSpeakingWhenAlertShouldBeActive) { sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); ASSERT_TRUE(checkSentEventName(sendParams, NAME_SET_ALERT_SUCCEEDED)); - // Write audio to SDS sying "Weather". - sendAudioFileAsRecognize(RECOGNIZE_WEATHER_AUDIO_FILE_NAME); + // Signal to the AIP to start recognizing. + ASSERT_NE(nullptr, m_HoldToTalkAudioProvider); + ASSERT_TRUE(m_holdToTalkButton->startRecognizing(m_AudioInputProcessor, m_HoldToTalkAudioProvider)); + + // Put audio onto the SDS saying "Tell me a joke". + bool error = false; + std::string file = inputPath + RECOGNIZE_WEATHER_AUDIO_FILE_NAME; + std::vector audioData = readAudioFromFile(file, &error); + ASSERT_FALSE(error); + ASSERT_FALSE(audioData.empty()); + m_AudioBufferWriter->write(audioData.data(), audioData.size()); + + sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); + ASSERT_TRUE(checkSentEventName(sendParams, NAME_RECOGNIZE)); // The test channel client has been notified the content channel has been backgrounded. - bool focusChanged; + bool focusChanged = false; ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::BACKGROUND); ASSERT_TRUE(focusChanged); + // AlertStarted Event is sent. sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(checkSentEventName(sendParams, NAME_RECOGNIZE)); + ASSERT_TRUE(checkSentEventName(sendParams, NAME_ALERT_STARTED)); + + // Stop holding the button. + ASSERT_TRUE(m_holdToTalkButton->stopRecognizing(m_AudioInputProcessor)); // Speech is handled. sendStartedParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - EXPECT_TRUE(checkSentEventName(sendStartedParams, NAME_SPEECH_STARTED)); - - // AlertStarted Event is sent. - sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - EXPECT_TRUE(checkSentEventName(sendParams, NAME_ALERT_STARTED)); + ASSERT_TRUE(checkSentEventName(sendStartedParams, NAME_SPEECH_STARTED)); sendFinishedParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); if(getSentEventName(sendParams) == NAME_SPEECH_FINISHED) { sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - EXPECT_TRUE(checkSentEventName(sendParams, NAME_ALERT_ENTERED_FOREGROUND)); + ASSERT_TRUE(checkSentEventName(sendParams, NAME_ALERT_ENTERED_FOREGROUND)); } else { - EXPECT_TRUE(checkSentEventName(sendFinishedParams, NAME_SPEECH_FINISHED)); + ASSERT_TRUE(checkSentEventName(sendFinishedParams, NAME_SPEECH_FINISHED)); } + std::this_thread::sleep_for(std::chrono::milliseconds(800)); + // Locally stop the alarm. m_alertsAgent->onLocalStop(); - // These steps removed as a workaround for our gstreamer implementation behaving incorrectly. // Low priority Test client gets back permission to the test channel - //EXPECT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::FOREGROUND); - //EXPECT_TRUE(focusChanged); + ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::FOREGROUND); + ASSERT_TRUE(focusChanged); - //m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged); - //ASSERT_FALSE(focusChanged); } TEST_F(AlertsTest, handleOneTimerWithVocalStop) { - TestMessageSender::SendParams sendSyncParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(checkSentEventName(sendSyncParams, NAME_SYNC_STATE)); // Write audio to SDS saying "Set a timer for 5 seconds" sendAudioFileAsRecognize(RECOGNIZE_TIMER_AUDIO_FILE_NAME); @@ -1255,7 +1256,7 @@ TEST_F(AlertsTest, handleOneTimerWithVocalStop) { std::this_thread::sleep_for(std::chrono::milliseconds(2000)); // The test channel client has been notified the content channel has been backgrounded. - bool focusChanged; + bool focusChanged = false; ASSERT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::BACKGROUND); ASSERT_TRUE(focusChanged); @@ -1281,14 +1282,16 @@ TEST_F(AlertsTest, handleOneTimerWithVocalStop) { } else { ASSERT_TRUE(checkSentEventName(sendParams, NAME_ALERT_STOPPED)); + } + sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); + ASSERT_TRUE(checkSentEventName(sendParams, NAME_DELETE_ALERT_SUCCEEDED)); + // Low priority Test client gets back permission to the test channel EXPECT_EQ(m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged), FocusState::FOREGROUND); EXPECT_TRUE(focusChanged); - m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged); - ASSERT_FALSE(focusChanged); } } // namespace test @@ -1307,4 +1310,4 @@ int main(int argc, char **argv) { alexaClientSDK::integration::test::inputPath = std::string(argv[2]); return RUN_ALL_TESTS(); } -} \ No newline at end of file +} diff --git a/Integration/test/AlexaCommunicationsLibraryTest.cpp b/Integration/test/AlexaCommunicationsLibraryTest.cpp index bbdb26d453..75ff6f07e3 100644 --- a/Integration/test/AlexaCommunicationsLibraryTest.cpp +++ b/Integration/test/AlexaCommunicationsLibraryTest.cpp @@ -198,6 +198,9 @@ class AlexaCommunicationsLibraryTest : public ::testing::Test { isEnabled, { m_connectionStatusObserver }, { m_clientMessageHandler }); + // TODO: ACSDK-421: Remove the callback when m_avsConnection manager is no longer an observer to + // StateSynchronizer. + m_avsConnectionManager->onStateChanged(StateSynchronizerObserverInterface::State::SYNCHRONIZED); connect(); } diff --git a/Integration/test/AlexaDirectiveSequencerLibraryTest.cpp b/Integration/test/AlexaDirectiveSequencerLibraryTest.cpp index 80bf01ce96..9a1c6e94e1 100644 --- a/Integration/test/AlexaDirectiveSequencerLibraryTest.cpp +++ b/Integration/test/AlexaDirectiveSequencerLibraryTest.cpp @@ -247,6 +247,9 @@ class AlexaDirectiveSequencerLibraryTest : public ::testing::Test { isEnabled, { m_connectionStatusObserver }, { m_messageInterpreter }); + // TODO: ACSDK-421: Remove the callback when m_avsConnection manager is no longer an observer to + // StateSynchronizer. + m_avsConnectionManager->onStateChanged(StateSynchronizerObserverInterface::State::SYNCHRONIZED); connect(); } @@ -620,9 +623,15 @@ TEST_F(AlexaDirectiveSequencerLibraryTest, sendDirectiveWithoutADialogRequestID) TestDirectiveHandler::DirectiveParams params; - // Make sure we get the handleImmediately from StopCapture. + // Make sure we get preHandle followed by handle for StopCapture. + params = directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); - ASSERT_TRUE(params.isHandleImmediately()); + ASSERT_TRUE(params.isPreHandle()); + ASSERT_TRUE(params.directive->getDialogRequestId().empty()); + ASSERT_EQ(params.directive->getName(), NAME_STOP_CAPTURE); + + params = directiveHandler->waitForNext(WAIT_FOR_TIMEOUT_DURATION); + ASSERT_TRUE(params.isHandle()); ASSERT_TRUE(params.directive->getDialogRequestId().empty()); ASSERT_EQ(params.directive->getName(), NAME_STOP_CAPTURE); diff --git a/Integration/test/AudioInputProcessorIntegrationTest.cpp b/Integration/test/AudioInputProcessorIntegrationTest.cpp index 7ccd317f13..e843361f94 100644 --- a/Integration/test/AudioInputProcessorIntegrationTest.cpp +++ b/Integration/test/AudioInputProcessorIntegrationTest.cpp @@ -56,6 +56,7 @@ #include "Integration/TestMessageSender.h" #include "Integration/TestDirectiveHandler.h" #include "Integration/TestExceptionEncounteredSender.h" +#include "System/UserInactivityMonitor.h" // If the tests are created with both Kittai and Sensory, Kittai is chosen. #ifdef KWD_KITTAI @@ -78,6 +79,7 @@ using namespace alexaClientSDK::avsCommon::avs::attachment; using namespace alexaClientSDK::avsCommon::sdkInterfaces; using namespace alexaClientSDK::avsCommon::avs::initialization; using namespace capabilityAgents::aip; +using namespace capabilityAgents::system; using namespace sdkInterfaces; using namespace avsCommon::utils::sds; using namespace avsCommon::utils::json; @@ -445,13 +447,17 @@ class AudioInputProcessorTest : public ::testing::Test { ASSERT_NE (nullptr, m_avsConnectionManager); connect(); + m_userInactivityMonitor = UserInactivityMonitor::create( + m_avsConnectionManager, + m_exceptionEncounteredSender); m_AudioInputProcessor = AudioInputProcessor::create( m_directiveSequencer, m_avsConnectionManager, m_contextManager, m_focusManager, m_dialogUXStateAggregator, - m_exceptionEncounteredSender + m_exceptionEncounteredSender, + m_userInactivityMonitor ); ASSERT_NE (nullptr, m_AudioInputProcessor); m_AudioInputProcessor->addObserver(m_dialogUXStateAggregator); @@ -554,6 +560,7 @@ class AudioInputProcessorTest : public ::testing::Test { std::shared_ptr m_focusManager; std::shared_ptr m_dialogUXStateAggregator; std::shared_ptr m_testClient; + std::shared_ptr m_userInactivityMonitor; std::shared_ptr m_AudioInputProcessor; std::shared_ptr m_StateObserver; std::shared_ptr m_tapToTalkButton; diff --git a/Integration/test/AudioPlayerIntegrationTest.cpp b/Integration/test/AudioPlayerIntegrationTest.cpp new file mode 100644 index 0000000000..a23b823372 --- /dev/null +++ b/Integration/test/AudioPlayerIntegrationTest.cpp @@ -0,0 +1,678 @@ +/* + * AudioPlayerIntegrationTest.cpp + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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. + */ + +/// @file AudioPlayerIntegrationTest.cpp +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ACL/Transport/HTTP2MessageRouter.h" +#include "ADSL/DirectiveSequencer.h" +#include "ADSL/MessageInterpreter.h" +#include "AFML/FocusManager.h" +#include "AIP/AudioInputProcessor.h" +#include "AIP/AudioProvider.h" +#include "AIP/Initiator.h" +#include "AudioPlayer/AudioPlayer.h" +#include "AuthDelegate/AuthDelegate.h" +#include "AVSCommon/AVS/Attachment/AttachmentManager.h" +#include "AVSCommon/AVS/Attachment/InProcessAttachmentReader.h" +#include "AVSCommon/AVS/Attachment/InProcessAttachmentWriter.h" +#include "AVSCommon/AVS/BlockingPolicy.h" +#include "AVSCommon/Utils/JSON/JSONUtils.h" +#include "AVSCommon/SDKInterfaces/DirectiveHandlerInterface.h" +#include "AVSCommon/SDKInterfaces/DirectiveHandlerResultInterface.h" +#include "AVSCommon/AVS/Initialization/AlexaClientSDKInit.h" +#include "AVSCommon/Utils/Logger/LogEntry.h" +#include "ContextManager/ContextManager.h" +#include "Integration/AuthObserver.h" +#include "Integration/ClientMessageHandler.h" +#include "Integration/ConnectionStatusObserver.h" +#include "Integration/ObservableMessageRequest.h" +#include "Integration/TestMessageSender.h" +#include "Integration/TestDirectiveHandler.h" +#include "Integration/TestExceptionEncounteredSender.h" +#include "Integration/TestSpeechSynthesizerObserver.h" +#include "SpeechSynthesizer/SpeechSynthesizer.h" +#include "System/StateSynchronizer.h" +#include "System/UserInactivityMonitor.h" + +#ifdef GSTREAMER_MEDIA_PLAYER +#include "MediaPlayer/MediaPlayer.h" +#else +#include "Integration/TestMediaPlayer.h" +#endif + +namespace alexaClientSDK { +namespace integration { +namespace test { + +using namespace acl; +using namespace adsl; +using namespace authDelegate; +using namespace avsCommon; +using namespace avsCommon::avs; +using namespace avsCommon::avs::attachment; +using namespace avsCommon::sdkInterfaces; +using namespace avsCommon::avs::initialization; +using namespace avsCommon::utils::mediaPlayer; +using namespace contextManager; +using namespace sdkInterfaces; +using namespace avsCommon::utils::sds; +using namespace avsCommon::utils::json; +using namespace capabilityAgents::aip; +using namespace afml; +using namespace capabilityAgents::speechSynthesizer; +using namespace capabilityAgents::system; +#ifdef GSTREAMER_MEDIA_PLAYER +using namespace mediaPlayer; +#endif + +// This is a 16 bit 16 kHz little endian linear PCM audio file of "Sing me a song" to be recognized. +static const std::string RECOGNIZE_SING_FILE_NAME = "/recognize_sing_song_test.wav"; +// This is a 16 bit 16 kHz little endian linear PCM audio file of "Flashbriefing" to be recognized. +static const std::string RECOGNIZE_FLASHBRIEFING_FILE_NAME = "/recognize_flashbriefing_test.wav"; + +// This string to be used for PlaybackStarted Directives which use the AudioPlayer namespace. +static const std::string NAME_PLAYBACK_STARTED = "PlaybackStarted"; +// This string to be used for PlaybackNearlyFinished Directives which use the AudioPlayer namespace. +static const std::string NAME_PLAYBACK_NEARLY_FINISHED = "PlaybackNearlyFinished"; +// This string to be used for PlaybackFinished Directives which use the AudioPlayer namespace. +static const std::string NAME_PLAYBACK_FINISHED = "PlaybackFinished"; +// This string to be used for PlaybackStopped Directives which use the AudioPlayer namespace. +static const std::string NAME_PLAYBACK_STOPPED = "PlaybackStopped"; +// This string to be used for SynchronizeState Directives. +static const std::string NAME_SYNC_STATE = "SynchronizeState"; +// This string to be used for Speak Directives which use the NAMESPACE_SPEECH_SYNTHESIZER namespace. +static const std::string NAME_RECOGNIZE = "Recognize"; +// This string to be used for SpeechStarted Directives which use the NAMESPACE_SPEECH_SYNTHESIZER namespace. +static const std::string NAME_SPEECH_STARTED = "SpeechStarted"; +// This string to be used for SpeechFinished Directives which use the NAMESPACE_SPEECH_SYNTHESIZER namespace. +static const std::string NAME_SPEECH_FINISHED = "SpeechFinished"; + +/// The dialog Channel name used in intializing the FocusManager. +static const std::string DIALOG_CHANNEL_NAME = "Dialog"; + +/// The content Channel name used in intializing the FocusManager. +static const std::string CONTENT_CHANNEL_NAME = "Content"; + +/// An incorrect Channel name that is never initialized as a Channel. +static const std::string TEST_CHANNEL_NAME = "Test"; + +/// The priority of the dialog Channel used in intializing the FocusManager. +static const unsigned int DIALOG_CHANNEL_PRIORITY = 100; + +/// The priority of the content Channel used in intializing the FocusManager. +static const unsigned int CONTENT_CHANNEL_PRIORITY = 300; + +/// The priority of the content Channel used in intializing the FocusManager. +static const unsigned int TEST_CHANNEL_PRIORITY = 400; + +/// Sample dialog activity id. +static const std::string DIALOG_ACTIVITY_ID = "dialog"; + +/// Sample content activity id. +static const std::string CONTENT_ACTIVITY_ID = "content"; + +/// Sample content activity id. +static const std::string TEST_ACTIVITY_ID = "test"; + +// This Integer to be used to specify a timeout in seconds. +static const std::chrono::seconds WAIT_FOR_TIMEOUT_DURATION(15); +static const std::chrono::seconds NO_TIMEOUT_DURATION(0); +static const std::chrono::seconds SONG_TIMEOUT_DURATION(120); +/// The compatible encoding for AIP. +static const avsCommon::utils::AudioFormat::Encoding COMPATIBLE_ENCODING = + avsCommon::utils::AudioFormat::Encoding::LPCM; +/// The compatible endianness for AIP. +static const avsCommon::utils::AudioFormat::Endianness COMPATIBLE_ENDIANNESS = + avsCommon::utils::AudioFormat::Endianness::LITTLE; +/// The compatible sample rate for AIP. +static const unsigned int COMPATIBLE_SAMPLE_RATE = 16000; +/// The compatible bits per sample for Kitt.ai. +static const unsigned int COMPATIBLE_SAMPLE_SIZE_IN_BITS = 16; +/// The compatible number of channels for Kitt.ai +static const unsigned int COMPATIBLE_NUM_CHANNELS = 1; + +/// JSON key to get the event object of a message. +static const std::string JSON_MESSAGE_EVENT_KEY = "event"; +/// JSON key to get the directive object of a message. +static const std::string JSON_MESSAGE_DIRECTIVE_KEY = "directive"; +/// JSON key to get the header object of a message. +static const std::string JSON_MESSAGE_HEADER_KEY = "header"; +/// JSON key to get the namespace value of a header. +static const std::string JSON_MESSAGE_NAMESPACE_KEY = "namespace"; +/// JSON key to get the name value of a header. +static const std::string JSON_MESSAGE_NAME_KEY = "name"; +/// JSON key to get the messageId value of a header. +static const std::string JSON_MESSAGE_MESSAGE_ID_KEY = "messageId"; +/// JSON key to get the dialogRequestId value of a header. +static const std::string JSON_MESSAGE_DIALOG_REQUEST_ID_KEY = "dialogRequestId"; +/// JSON key to get the payload object of a message. +static const std::string JSON_MESSAGE_PAYLOAD_KEY = "payload"; + +/// String to identify log entries originating from this file. +static const std::string TAG("AudioPlayerIntegrationTest"); + +/** + * Create a LogEntry using this file's TAG and the specified event string. + * + * @param The event string for this @c LogEntry. + */ +#define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) + +std::string configPath; +std::string inputPath; + +/// A test observer that mocks out the ChannelObserverInterface##onFocusChanged() call. +class TestClient : public ChannelObserverInterface { +public: + /** + * Constructor. + */ + TestClient() : + m_focusState(FocusState::NONE), m_focusChangeOccurred(false) { + } + + /** + * Implementation of the ChannelObserverInterface##onFocusChanged() callback. + * + * @param focusState The new focus state of the Channel observer. + */ + void onFocusChanged(FocusState focusState) override { + std::unique_lock lock(m_mutex); + m_focusState = focusState; + m_focusChangeOccurred = true; + m_focusChangedCV.notify_one(); + } + + /** + * Waits for the ChannelObserverInterface##onFocusChanged() callback. + * + * @param timeout The amount of time to wait for the callback. + * @param focusChanged An output parameter that notifies the caller whether a callback occurred. + * @return Returns @c true if the callback occured within the timeout period and @c false otherwise. + */ + FocusState waitForFocusChange(std::chrono::milliseconds timeout, bool* focusChanged) { + std::unique_lock lock(m_mutex); + bool success = m_focusChangedCV.wait_for(lock, timeout, [this] () { + return m_focusChangeOccurred; + }); + + if (!success) { + *focusChanged = false; + } else { + m_focusChangeOccurred = false; + *focusChanged = true; + } + return m_focusState; + } + +private: + /// The focus state of the observer. + FocusState m_focusState; + + /// A lock to guard against focus state changes. + std::mutex m_mutex; + + /// A condition variable to wait for focus changes. + std::condition_variable m_focusChangedCV; + + /// A boolean flag so that we can re-use the observer even after a callback has occurred. + bool m_focusChangeOccurred; +}; + +class holdToTalkButton{ +public: + bool startRecognizing(std::shared_ptr aip, + std::shared_ptr audioProvider) { + return aip->recognize(*audioProvider, Initiator::PRESS_AND_HOLD).get(); + } + + bool stopRecognizing(std::shared_ptr aip) { + return aip->stopCapture().get(); + } +}; + +class AudioPlayerTest : public ::testing::Test { +protected: + + virtual void SetUp() override { + + std::ifstream infile(configPath); + ASSERT_TRUE(infile.good()); + ASSERT_TRUE(AlexaClientSDKInit::initialize({&infile})); + m_authObserver = std::make_shared(); + m_authDelegate = AuthDelegate::create(); + m_authDelegate->addAuthObserver(m_authObserver); + m_attachmentManager = std::make_shared( + AttachmentManager::AttachmentType::IN_PROCESS); + m_connectionStatusObserver = std::make_shared(); + bool isEnabled = false; + m_messageRouter = std::make_shared(m_authDelegate, m_attachmentManager); + m_exceptionEncounteredSender = std::make_shared(); + m_dialogUXStateAggregator = std::make_shared(); + + m_directiveSequencer = DirectiveSequencer::create(m_exceptionEncounteredSender); + m_messageInterpreter = std::make_shared( + m_exceptionEncounteredSender, + m_directiveSequencer, + m_attachmentManager); + + // Set up connection and connect + m_avsConnectionManager = std::make_shared( + m_messageRouter, + isEnabled, + m_connectionStatusObserver, + m_messageInterpreter); + ASSERT_NE (nullptr, m_avsConnectionManager); + + FocusManager::ChannelConfiguration dialogChannelConfig{DIALOG_CHANNEL_NAME, DIALOG_CHANNEL_PRIORITY}; + FocusManager::ChannelConfiguration contentChannelConfig{CONTENT_CHANNEL_NAME, CONTENT_CHANNEL_PRIORITY}; + FocusManager::ChannelConfiguration testChannelConfig{TEST_CHANNEL_NAME, TEST_CHANNEL_PRIORITY}; + + std::vector channelConfigurations { + dialogChannelConfig, contentChannelConfig, testChannelConfig + }; + + m_focusManager = std::make_shared(channelConfigurations); + + m_testContentClient = std::make_shared(); + ASSERT_TRUE(m_focusManager->acquireChannel(TEST_CHANNEL_NAME, m_testContentClient, TEST_ACTIVITY_ID)); + bool focusChanged; + FocusState state; + state = m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged); + ASSERT_TRUE(focusChanged); + ASSERT_EQ(state, FocusState::FOREGROUND); + + + m_contextManager = ContextManager::create(); + ASSERT_NE (nullptr, m_contextManager); + +#ifdef GSTREAMER_MEDIA_PLAYER + m_speakMediaPlayer = MediaPlayer::create(); +#else + m_speakMediaPlayer = std::make_shared(); +#endif + + m_compatibleAudioFormat.sampleRateHz = COMPATIBLE_SAMPLE_RATE; + m_compatibleAudioFormat.sampleSizeInBits = COMPATIBLE_SAMPLE_SIZE_IN_BITS; + m_compatibleAudioFormat.numChannels = COMPATIBLE_NUM_CHANNELS; + m_compatibleAudioFormat.endianness = COMPATIBLE_ENDIANNESS; + m_compatibleAudioFormat.encoding = COMPATIBLE_ENCODING; + + size_t nWords = 1024*1024; + size_t wordSize = 2; + size_t maxReaders = 3; + size_t bufferSize = AudioInputStream::calculateBufferSize(nWords, wordSize, maxReaders); + + auto m_Buffer = std::make_shared(bufferSize); + auto m_Sds = avsCommon::avs::AudioInputStream::create(m_Buffer, wordSize, maxReaders); + ASSERT_NE (nullptr, m_Sds); + m_AudioBuffer = std::move(m_Sds); + m_AudioBufferWriter = m_AudioBuffer->createWriter( + avsCommon::avs::AudioInputStream::Writer::Policy::NONBLOCKABLE); + ASSERT_NE (nullptr, m_AudioBufferWriter); + + // Set up hold to talk button. + bool alwaysReadable = true; + bool canOverride = true; + bool canBeOverridden = true; + m_HoldToTalkAudioProvider = std::make_shared( m_AudioBuffer, m_compatibleAudioFormat, + ASRProfile::CLOSE_TALK, !alwaysReadable, canOverride, !canBeOverridden); + + m_holdToTalkButton = std::make_shared(); + + m_userInactivityMonitor = UserInactivityMonitor::create( + m_avsConnectionManager, + m_exceptionEncounteredSender); + m_AudioInputProcessor = AudioInputProcessor::create( + m_directiveSequencer, + m_avsConnectionManager, + m_contextManager, + m_focusManager, + m_dialogUXStateAggregator, + m_exceptionEncounteredSender, + m_userInactivityMonitor + ); + ASSERT_NE (nullptr, m_AudioInputProcessor); + m_AudioInputProcessor->addObserver(m_dialogUXStateAggregator); + + // Create and register the SpeechSynthesizer. + m_speechSynthesizer = SpeechSynthesizer::create( + m_speakMediaPlayer, + m_avsConnectionManager, + m_focusManager, + m_contextManager, + m_attachmentManager, + m_exceptionEncounteredSender); + ASSERT_NE(nullptr, m_speechSynthesizer); + m_directiveSequencer->addDirectiveHandler(m_speechSynthesizer); + m_speechSynthesizerObserver = std::make_shared(); + m_speechSynthesizer->addObserver(m_speechSynthesizerObserver); + + // TODO: ACSDK-421: Revert this to use m_connectionManager rather than m_messageRouter. + m_stateSynchronizer = StateSynchronizer::create( + m_contextManager, + m_messageRouter); + ASSERT_NE(nullptr, m_stateSynchronizer); + m_avsConnectionManager->addConnectionStatusObserver(m_stateSynchronizer); + + +#ifdef GSTREAMER_MEDIA_PLAYER + m_contentMediaPlayer = MediaPlayer::create(); +#else + m_contentMediaPlayer = std::make_shared(); +#endif + + // Create and register the AudioPlayer. + m_audioPlayer = capabilityAgents::audioPlayer::AudioPlayer::create( + m_contentMediaPlayer, + m_avsConnectionManager, + m_focusManager, + m_contextManager, + m_attachmentManager, + m_exceptionEncounteredSender); + ASSERT_NE(nullptr, m_audioPlayer); + m_directiveSequencer->addDirectiveHandler(m_audioPlayer); + + connect(); + + } + + void TearDown() override { + disconnect(); + if (m_audioPlayer) { + m_audioPlayer->shutdown(); + } + m_AudioInputProcessor->resetState().wait(); + m_directiveSequencer->shutdown(); + + AlexaClientSDKInit::uninitialize(); + } + + /** + * Connect to AVS. + */ + void connect() { + ASSERT_TRUE(m_authObserver->waitFor(AuthObserver::State::REFRESHED)) + << "Retrieving the auth token timed out."; + m_avsConnectionManager->enable(); + ASSERT_TRUE(m_connectionStatusObserver->waitFor(ConnectionStatusObserverInterface::Status::CONNECTED)) + << "Connecting timed out."; + } + + /** + * Disconnect from AVS. + */ + void disconnect() { + m_avsConnectionManager->disable(); + ASSERT_TRUE(m_connectionStatusObserver->waitFor(ConnectionStatusObserverInterface::Status::DISCONNECTED)) + << "Connecting timed out."; + } + + std::string getSentEventName(TestMessageSender::SendParams sendParams) { + std::string eventString; + std::string eventHeader; + std::string eventName; + jsonUtils::lookupStringValue(sendParams.request->getJsonContent(), JSON_MESSAGE_EVENT_KEY, &eventString); + jsonUtils::lookupStringValue(eventString, JSON_MESSAGE_HEADER_KEY, &eventHeader); + jsonUtils::lookupStringValue(eventHeader, JSON_MESSAGE_NAME_KEY, &eventName); + return eventName; + } + + bool checkSentEventName(TestMessageSender::SendParams sendParams, std::string expectedName) { + if (TestMessageSender::SendParams::Type::SEND == sendParams.type) { + std::string eventName; + eventName = getSentEventName(sendParams); + return eventName == expectedName; + } + return false; + } + + std::vector readAudioFromFile(const std::string &fileName, bool* errorOccurred) { + const int RIFF_HEADER_SIZE = 44; + + std::ifstream inputFile(fileName.c_str(), std::ifstream::binary); + if (!inputFile.good()) { + std::cout << "Couldn't open audio file!" << std::endl; + if (errorOccurred) { + *errorOccurred = true; + } + return {}; + } + inputFile.seekg(0, std::ios::end); + int fileLengthInBytes = inputFile.tellg(); + if (fileLengthInBytes <= RIFF_HEADER_SIZE) { + std::cout << "File should be larger than 44 bytes, which is the size of the RIFF header" << std::endl; + if (errorOccurred) { + *errorOccurred = true; + } + return {}; + } + + inputFile.seekg(RIFF_HEADER_SIZE, std::ios::beg); + + int numSamples = (fileLengthInBytes - RIFF_HEADER_SIZE) / 2; + + std::vector retVal(numSamples, 0); + + inputFile.read((char *)&retVal[0], numSamples * 2); + + if (inputFile.gcount() != numSamples*2) { + std::cout << "Error reading audio file" << std::endl; + if (errorOccurred) { + *errorOccurred = true; + } + return {}; + } + + inputFile.close(); + if (errorOccurred) { + *errorOccurred = false; + } + return retVal; + } + + void sendAudioFileAsRecognize(std::string audioFile) { + // Signal to the AIP to start recognizing. + ASSERT_NE(nullptr, m_HoldToTalkAudioProvider); + ASSERT_TRUE(m_holdToTalkButton->startRecognizing(m_AudioInputProcessor, m_HoldToTalkAudioProvider)); + + bool error; + std::string file = inputPath + audioFile; + std::vector audioData = readAudioFromFile(file, &error); + ASSERT_FALSE(error); + ASSERT_FALSE(audioData.empty()); + m_AudioBufferWriter->write(audioData.data(), audioData.size()); + + // Stop holding the button. + ASSERT_TRUE(m_holdToTalkButton->stopRecognizing(m_AudioInputProcessor)); + } + + std::shared_ptr m_authObserver; + std::shared_ptr m_authDelegate; + std::shared_ptr m_connectionStatusObserver; + std::shared_ptr m_messageRouter; + std::shared_ptr m_avsConnectionManager; + std::shared_ptr m_exceptionEncounteredSender; + std::shared_ptr m_directiveHandler; + std::shared_ptr m_directiveSequencer; + std::shared_ptr m_messageInterpreter; + std::shared_ptr m_contextManager; + std::shared_ptr m_attachmentManager; + std::shared_ptr m_focusManager; + std::shared_ptr m_testContentClient; + std::shared_ptr m_speechSynthesizer; + std::shared_ptr m_speechSynthesizerObserver; + std::shared_ptr m_stateSynchronizer; + std::shared_ptr m_holdToTalkButton; + std::shared_ptr m_HoldToTalkAudioProvider; + avsCommon::utils::AudioFormat m_compatibleAudioFormat; + std::unique_ptr m_AudioBufferWriter; + std::shared_ptr m_AudioBuffer; + std::shared_ptr m_AudioInputProcessor; + std::shared_ptr m_userInactivityMonitor; + std::shared_ptr m_audioPlayer; + + FocusState m_focusState; + std::mutex m_mutex; + std::condition_variable m_focusChangedCV; + bool m_focusChangeOccurred; + std::shared_ptr m_dialogUXStateAggregator; + +#ifdef GSTREAMER_MEDIA_PLAYER + std::shared_ptr m_speakMediaPlayer; + std::shared_ptr m_contentMediaPlayer; +#else + std::shared_ptr m_speakMediaPlayer; + std::shared_ptr m_contentMediaPlayer; +#endif + +}; + +/** + * Test ability for the AudioPlayer to handle one play directive. + * + * This test is intended to test the AudioPlayer's ability to handle a short play directive all the way through. To do this, + * an audio file of "Sing me a song" is sent as a Recognize event. In response, a Play directive is received. The tests then + * observe that the correct events are sent in order. + * + */ +TEST_F(AudioPlayerTest, SingASong) { + // Sing me a song. + sendAudioFileAsRecognize(RECOGNIZE_SING_FILE_NAME); + bool focusChanged; + FocusState state; + state = m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged); + ASSERT_TRUE(focusChanged); + ASSERT_EQ(state, FocusState::BACKGROUND); + + // Recognize. + TestMessageSender::SendParams sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); + ASSERT_TRUE(checkSentEventName(sendParams, NAME_RECOGNIZE)); + + // PlaybackStarted + sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); + ASSERT_TRUE(checkSentEventName(sendParams, NAME_PLAYBACK_STARTED)); + + // PlaybackNearlyFinished + sendParams = m_avsConnectionManager->waitForNext(SONG_TIMEOUT_DURATION); + ASSERT_TRUE(checkSentEventName(sendParams, NAME_PLAYBACK_NEARLY_FINISHED)); + + sendParams = m_avsConnectionManager->waitForNext(SONG_TIMEOUT_DURATION); + ASSERT_TRUE(checkSentEventName(sendParams, NAME_PLAYBACK_FINISHED)); + + state = m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged); + ASSERT_TRUE(focusChanged); + ASSERT_EQ(state, FocusState::FOREGROUND); + + m_testContentClient->waitForFocusChange(NO_TIMEOUT_DURATION, &focusChanged); + ASSERT_FALSE(focusChanged); +} + +/** + * Test ability for the AudioPlayer to handle multiple play directives. + * + * This test is intended to test the AudioPlayer's ability to handle a group play directive all the way through. To do this, + * an audio file of "Flashbriefing" is sent as a Recognize event. In response, a Speak, then an undefined number of Play + * directives, and a final Speak directive is received. The tests then observe that the correct events are sent in order. + * + */ +TEST_F(AudioPlayerTest, FlashBriefing) { + // Ask for a flashbriefing. + sendAudioFileAsRecognize(RECOGNIZE_FLASHBRIEFING_FILE_NAME); + + bool focusChanged; + FocusState state; + state = m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged); + ASSERT_TRUE(focusChanged); + ASSERT_EQ(state, FocusState::BACKGROUND); + + // Recognize event is sent. + TestMessageSender::SendParams sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); + ASSERT_TRUE(checkSentEventName(sendParams, NAME_RECOGNIZE)); + + state = m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged); + ASSERT_TRUE(focusChanged); + ASSERT_EQ(state, FocusState::FOREGROUND); + + state = m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged); + if(focusChanged) { + ASSERT_EQ(state, FocusState::BACKGROUND); + } + + // Speech is handled. + TestMessageSender::SendParams sendStartedParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); + ASSERT_TRUE(checkSentEventName(sendStartedParams, NAME_SPEECH_STARTED)); + TestMessageSender::SendParams sendFinishedParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); + ASSERT_TRUE(checkSentEventName(sendFinishedParams, NAME_SPEECH_FINISHED)); + + // If no items are in flashbriefing, this section will be skipped. Ensure that at least two items are selected in the + // Alexa app under Settings -> Flashbriefing. + sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); + bool hasFlashbriefingItems = false; + while (TestMessageSender::SendParams::Type::TIMEOUT != sendParams.type + && !checkSentEventName(sendParams, NAME_SPEECH_STARTED) + && !checkSentEventName(sendParams, NAME_PLAYBACK_STOPPED)) { + hasFlashbriefingItems = true; + ASSERT_TRUE(checkSentEventName(sendParams, NAME_PLAYBACK_STARTED)); + sendParams = m_avsConnectionManager->waitForNext(SONG_TIMEOUT_DURATION); + ASSERT_TRUE(checkSentEventName(sendParams, NAME_PLAYBACK_NEARLY_FINISHED)); + sendParams = m_avsConnectionManager->waitForNext(SONG_TIMEOUT_DURATION); + ASSERT_TRUE(checkSentEventName(sendParams, NAME_PLAYBACK_FINISHED)); + sendParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); + } + + if (hasFlashbriefingItems) { + // The last speak is then allowed. + sendStartedParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); + EXPECT_TRUE(checkSentEventName(sendStartedParams, NAME_SPEECH_STARTED)); + sendFinishedParams = m_avsConnectionManager->waitForNext(WAIT_FOR_TIMEOUT_DURATION); + EXPECT_TRUE(checkSentEventName(sendFinishedParams, NAME_SPEECH_FINISHED)); + } + + state = m_testContentClient->waitForFocusChange(WAIT_FOR_TIMEOUT_DURATION, &focusChanged); + ASSERT_EQ(state, FocusState::FOREGROUND); + + m_testContentClient->waitForFocusChange(NO_TIMEOUT_DURATION, &focusChanged); + EXPECT_FALSE(focusChanged); +} + +} // namespace test +} // namespace integration +} // namespace alexaClientSDK + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + if (argc < 3) { + std::cerr << "USAGE: AudioPlayerIntegration " + << std::endl; + return 1; + + } else { + alexaClientSDK::integration::test::configPath = std::string(argv[1]); + alexaClientSDK::integration::test::inputPath = std::string(argv[2]); + return RUN_ALL_TESTS(); + } +} diff --git a/Integration/test/CMakeLists.txt b/Integration/test/CMakeLists.txt index 46bf0553d9..84c3f274e4 100644 --- a/Integration/test/CMakeLists.txt +++ b/Integration/test/CMakeLists.txt @@ -14,10 +14,11 @@ set(INCLUDE_PATH "${ACL_SOURCE_DIR}/include" "${AVSSystem_SOURCE_DIR}/include" "${GST_INCLUDE_DIRS}" "${AVSCommon_INCLUDE_DIRS}" + "${AudioPlayer_INCLUDE_DIRS}" "${MediaPlayer_SOURCE_DIR}/include" "${CONTEXTMANAGER_SOURCE_DIR}/include") - set(LINK_PATH ACL AuthDelegate AFML ADSL AIP Alerts AVSSystem ContextManager KWD SpeechSynthesizer Integration gtest gmock) + set(LINK_PATH ACL AuthDelegate AFML ADSL AIP Alerts AudioPlayer AVSSystem ContextManager KWD SpeechSynthesizer Integration gtest gmock) if(KITTAI_KEY_WORD_DETECTOR) SET(LINK_PATH ${LINK_PATH} KITTAI) @@ -54,7 +55,8 @@ if(BUILD_TESTING) "${CMAKE_CURRENT_SOURCE_DIR}/AlexaDirectiveSequencerLibraryTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AudioInputProcessorIntegrationTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/SpeechSynthesizerIntegrationTest.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AlertsIntegrationTest.cpp") + "${CMAKE_CURRENT_SOURCE_DIR}/AlertsIntegrationTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AudioPlayerIntegrationTest.cpp") # file(GLOB_RECURSE testSourceFiles RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "*Test.cpp") foreach (testSourceFile IN LISTS testSourceFiles) get_filename_component(testName ${testSourceFile} NAME_WE) @@ -74,6 +76,7 @@ if(BUILD_TESTING) COMMAND AudioInputProcessorIntegrationTest "${SDK_CONFIG_FILE_TARGET}" "${INTEGRATION_INPUTS}" COMMAND SpeechSynthesizerIntegrationTest "${SDK_CONFIG_FILE_TARGET}" "${INTEGRATION_INPUTS}" COMMAND AlertsIntegrationTest "${SDK_CONFIG_FILE_TARGET}" "${INTEGRATION_INPUTS}" + COMMAND AudioPlayerIntegrationTest "${SDK_CONFIG_FILE_TARGET}" "${INTEGRATION_INPUTS}" COMMAND ${CTEST_CUSTOM_POST_TEST}) message(STATUS "Please fill ${SDK_CONFIG_FILE_TARGET} before you execute integration tests.") endif() diff --git a/MediaPlayer/include/MediaPlayer/AttachmentReaderSource.h b/MediaPlayer/include/MediaPlayer/AttachmentReaderSource.h index fbf04c3c42..f43543e302 100644 --- a/MediaPlayer/include/MediaPlayer/AttachmentReaderSource.h +++ b/MediaPlayer/include/MediaPlayer/AttachmentReaderSource.h @@ -43,7 +43,7 @@ class AttachmentReaderSource : public BaseStreamSource { */ static std::unique_ptr create( PipelineInterface* pipeline, - std::unique_ptr attachmentReader); + std::shared_ptr attachmentReader); ~AttachmentReaderSource(); @@ -56,7 +56,7 @@ class AttachmentReaderSource : public BaseStreamSource { */ AttachmentReaderSource( PipelineInterface* pipeline, - std::unique_ptr attachmentReader); + std::shared_ptr attachmentReader); /// @name Overridden BaseStreamSource methods. /// @{ @@ -67,7 +67,7 @@ class AttachmentReaderSource : public BaseStreamSource { private: /// The @c AttachmentReader to read audioData from. - std::unique_ptr m_reader; + std::shared_ptr m_reader; }; } // namespace mediaPlayer diff --git a/MediaPlayer/include/MediaPlayer/BaseStreamSource.h b/MediaPlayer/include/MediaPlayer/BaseStreamSource.h index 3ac1e05062..6ab3105b98 100644 --- a/MediaPlayer/include/MediaPlayer/BaseStreamSource.h +++ b/MediaPlayer/include/MediaPlayer/BaseStreamSource.h @@ -42,6 +42,10 @@ class BaseStreamSource : public SourceInterface { ~BaseStreamSource() override; + bool hasAdditionalData() override; + bool handleEndOfStream() override; + void preprocess() override; + protected: /** * Initializes a source. Creates all the necessary pipeline elements such that audio output from the final @@ -159,6 +163,21 @@ class BaseStreamSource : public SourceInterface { /// Function to invoke on the worker thread thread when there is enough data. const std::function m_handleEnoughDataFunction; + + /// ID of the handler installed to receive need data signals. + guint m_needDataHandlerId; + + /// ID of the handler installed to receive enough data signals. + guint m_enoughDataHandlerId; + + /// Mutex to serialize access to idle callback IDs. + std::mutex m_callbackIdMutex; + + /// ID of idle callback to handle need data. + guint m_needDataCallbackId; + + /// ID of idle callback to handle enough data. + guint m_enoughDataCallbackId; }; } // namespace mediaPlayer diff --git a/MediaPlayer/include/MediaPlayer/IStreamSource.h b/MediaPlayer/include/MediaPlayer/IStreamSource.h index 6dae1e74f7..77164d531d 100644 --- a/MediaPlayer/include/MediaPlayer/IStreamSource.h +++ b/MediaPlayer/include/MediaPlayer/IStreamSource.h @@ -42,7 +42,7 @@ class IStreamSource : public BaseStreamSource { */ static std::unique_ptr create( PipelineInterface* pipeline, - std::unique_ptr stream, + std::shared_ptr stream, bool repeat); /** @@ -58,7 +58,7 @@ class IStreamSource : public BaseStreamSource { * @param stream The @c std::istream from which to create the pipeline source. * @param repeat Whether the stream should be replayed until stopped. */ - IStreamSource(PipelineInterface* pipeline, std::unique_ptr stream, bool repeat); + IStreamSource(PipelineInterface* pipeline, std::shared_ptr stream, bool repeat); /// @name Overridden BaseStreamSource methods. /// @{ @@ -68,8 +68,8 @@ class IStreamSource : public BaseStreamSource { /// @} private: - /// The AttachmentReader to read audioData from. - std::unique_ptr m_stream; + /// The std::istream to read audioData from. + std::shared_ptr m_stream; /// Play the stream over and over until told to stop. bool m_repeat; diff --git a/MediaPlayer/include/MediaPlayer/MediaPlayer.h b/MediaPlayer/include/MediaPlayer/MediaPlayer.h index 55d00d947a..a1519feb93 100644 --- a/MediaPlayer/include/MediaPlayer/MediaPlayer.h +++ b/MediaPlayer/include/MediaPlayer/MediaPlayer.h @@ -25,12 +25,14 @@ #include #include #include +#include #include #include #include #include +#include #include "MediaPlayer/PipelineInterface.h" #include "MediaPlayer/SourceInterface.h" @@ -41,7 +43,9 @@ namespace mediaPlayer { /** * Class that handles creation of audio pipeline and playing of audio data. */ -class MediaPlayer : public avsCommon::utils::mediaPlayer::MediaPlayerInterface, private PipelineInterface { +class MediaPlayer : + public avsCommon::utils::mediaPlayer::MediaPlayerInterface, + private PipelineInterface { public: /** * Creates an instance of the @c MediaPlayer. @@ -55,26 +59,34 @@ class MediaPlayer : public avsCommon::utils::mediaPlayer::MediaPlayerInterface, */ ~MediaPlayer(); - /// @name Overridden @c MediaPlayerInterface methods. + /// @name Overridden MediaPlayerInterface methods. /// @{ avsCommon::utils::mediaPlayer::MediaPlayerStatus setSource( - std::unique_ptr attachmentReader) override; + std::shared_ptr attachmentReader) override; avsCommon::utils::mediaPlayer::MediaPlayerStatus setSource( - std::unique_ptr stream, bool repeat) override; + std::shared_ptr stream, bool repeat) override; + avsCommon::utils::mediaPlayer::MediaPlayerStatus setSource(const std::string& url) override; + avsCommon::utils::mediaPlayer::MediaPlayerStatus play() override; avsCommon::utils::mediaPlayer::MediaPlayerStatus stop() override; + avsCommon::utils::mediaPlayer::MediaPlayerStatus pause() override; + /** + * To resume playback after a pause, call @c resume. Calling @c play + * will reset the pipeline and source, and will not resume playback. + */ + avsCommon::utils::mediaPlayer::MediaPlayerStatus resume() override; int64_t getOffsetInMilliseconds() override; void setObserver(std::shared_ptr observer) override; /// @} - /// @name Overridden @c PipelineInterface methods. + /// @name Overridden PipelineInterface methods. /// @{ void setAppSrc(GstAppSrc* appSrc) override; GstAppSrc* getAppSrc() const override; void setDecoder(GstElement* decoder) override; GstElement* getDecoder() const override; GstElement* getPipeline() const override; - void queueCallback(const std::function* callback) override; + guint queueCallback(const std::function* callback) override; /// @} private: @@ -104,6 +116,15 @@ class MediaPlayer : public avsCommon::utils::mediaPlayer::MediaPlayerInterface, /// Pipeline element. GstElement* pipeline; + + /// Constructor. + AudioPipeline() + : + appsrc{nullptr}, + decoder{nullptr}, + converter{nullptr}, + audioSink{nullptr}, + pipeline{nullptr} {}; }; /** @@ -145,6 +166,11 @@ class MediaPlayer : public avsCommon::utils::mediaPlayer::MediaPlayerInterface, */ void tearDownPipeline(); + /* + * Resets the @c AudioPipeline. + */ + void resetPipeline(); + /** * This handles linking the source pad of the decoder to the sink pad of the converter once the pad-added signal * has been emitted by the decoder element. @@ -196,7 +222,11 @@ class MediaPlayer : public avsCommon::utils::mediaPlayer::MediaPlayerInterface, */ void handleSetAttachmentReaderSource( std::promise* promise, - std::unique_ptr reader); + std::shared_ptr reader); + + void handleSetSource( + std::promise promise, + std::string url); /** * Worker thread handler for setting the source of audio to play. @@ -207,7 +237,7 @@ class MediaPlayer : public avsCommon::utils::mediaPlayer::MediaPlayerInterface, */ void handleSetIStreamSource( std::promise* promise, - std::unique_ptr stream, + std::shared_ptr stream, bool repeat); /** @@ -233,6 +263,22 @@ class MediaPlayer : public avsCommon::utils::mediaPlayer::MediaPlayerInterface, */ avsCommon::utils::mediaPlayer::MediaPlayerStatus doStop(); + /** + * Worker thread handler for pausing playback of the current audio source. + * + * @param promise A promise to fulfill with a @c MediaPlayerStatus value once playback has been paused + * (or the operation has failed). + */ + void handlePause(std::promise* promise); + + /** + * Worker thread handler for resume playback of the current audio source. + * + * @param promise A promise to fulfill with a @c MediaPlayerStatus value once playback has been resumed + * (or the operation has failed). + */ + void handleResume(std::promise* promise); + /** * Worker thread handler for getting the current playback position. * @@ -260,6 +306,16 @@ class MediaPlayer : public avsCommon::utils::mediaPlayer::MediaPlayerInterface, */ void sendPlaybackFinished(); + /** + * Sends the playback paused notification to the observer. + */ + void sendPlaybackPaused(); + + /** + * Sends the playback resumed notification to the observer. + */ + void sendPlaybackResumed(); + /** * Sends the playback error notification to the observer. * @@ -267,6 +323,16 @@ class MediaPlayer : public avsCommon::utils::mediaPlayer::MediaPlayerInterface, */ void sendPlaybackError(const std::string& error); + /** + * Sends the buffer underrun notification to the observer. + */ + void sendBufferUnderrun(); + + /** + * Sends the buffer refilled notification to the observer. + */ + void sendBufferRefilled(); + /// An instance of the @c AudioPipeline. AudioPipeline m_pipeline; @@ -276,17 +342,29 @@ class MediaPlayer : public avsCommon::utils::mediaPlayer::MediaPlayerInterface, // Main loop thread std::thread m_mainLoopThread; + // Set Source thread. + std::thread m_setSourceThread; + /// Bus Id to track the bus. guint m_busWatchId; + /// Flag to indicate when a playback started notification has been sent to the observer. + bool m_playbackStartedSent; + /// Flag to indicate when a playback finished notification has been sent to the observer. bool m_playbackFinishedSent; + /// Flag to indicate whether a playback is paused. + bool m_isPaused; + + /// Flag to indicate whether a buffer underrun is occurring. + bool m_isBufferUnderrun; + /// @c MediaPlayerObserverInterface instance to notify when the playback state changes. std::shared_ptr m_playerObserver; /// @c SourceInterface instance set to the appropriate source. - std::unique_ptr m_source; + std::shared_ptr m_source; }; } // namespace mediaPlayer diff --git a/MediaPlayer/include/MediaPlayer/PipelineInterface.h b/MediaPlayer/include/MediaPlayer/PipelineInterface.h index f9a493ca93..6cbce2274a 100644 --- a/MediaPlayer/include/MediaPlayer/PipelineInterface.h +++ b/MediaPlayer/include/MediaPlayer/PipelineInterface.h @@ -75,8 +75,9 @@ class PipelineInterface { * Queue the specified callback for execution on the worker thread. * * @param callback The callback to queue. + * @return The ID of the queued callback (for calling @c g_source_remove). */ - virtual void queueCallback(const std::function* callback) = 0; + virtual guint queueCallback(const std::function* callback) = 0; protected: /** diff --git a/MediaPlayer/include/MediaPlayer/SourceInterface.h b/MediaPlayer/include/MediaPlayer/SourceInterface.h index 81ea3b06b6..05807503b0 100644 --- a/MediaPlayer/include/MediaPlayer/SourceInterface.h +++ b/MediaPlayer/include/MediaPlayer/SourceInterface.h @@ -32,9 +32,31 @@ class SourceInterface { * Destructor. */ virtual ~SourceInterface() = default; + + /** + * Internally, a source may need additional processing after EOS is reached. + * This function will process that data. + * + * @return A boolean indicating whether the process operation was successful. + */ + virtual bool handleEndOfStream() = 0; + + /** + * Internally, a source may have additional data after processing an EOS. + * This function indicates whether there is additional data, and should be called + * after @c handleEndOfStream(). + * + * @return A boolean indicating whether the source has additional data to be played. + */ + virtual bool hasAdditionalData() = 0; + + /** + * Perform preprocessing of the source. Must be called before reading from the source. + */ + virtual void preprocess() = 0; }; } // namespace mediaPlayer } // namespace alexaClientSDK -#endif // ALEXA_CLIENT_SDK_MEDIA_PLAYER_INCLUDE_MEDIA_PLAYER_SOURCE_INTERFACE_H_ \ No newline at end of file +#endif // ALEXA_CLIENT_SDK_MEDIA_PLAYER_INCLUDE_MEDIA_PLAYER_SOURCE_INTERFACE_H_ diff --git a/MediaPlayer/include/MediaPlayer/UrlSource.h b/MediaPlayer/include/MediaPlayer/UrlSource.h new file mode 100644 index 0000000000..7193f6c237 --- /dev/null +++ b/MediaPlayer/include/MediaPlayer/UrlSource.h @@ -0,0 +1,113 @@ +/* + * UrlSource.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_MEDIA_PLAYER_INCLUDE_MEDIA_PLAYER_URL_SOURCE_H_ +#define ALEXA_CLIENT_SDK_MEDIA_PLAYER_INCLUDE_MEDIA_PLAYER_URL_SOURCE_H_ + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "MediaPlayer/SourceInterface.h" + +namespace alexaClientSDK { +namespace mediaPlayer { + +class UrlSource : + public SourceInterface, + public avsCommon::utils::playlistParser::PlaylistParserObserverInterface, + public std::enable_shared_from_this { +public: + /** + * Creates an instance of the @c UrlSource and installs the source within the GStreamer pipeline. + * + * @param pipeline The @c PipelineInterface through which the source of the @c AudioPipeline may be set. + * @param playlistParser The @c PlaylistParserInterface which will parse playlist urls. + * @param url The url from which to create the pipeline source from. + * + * @return An instance of the @c UrlSource if successful else a @c nullptr. + */ + static std::shared_ptr create( + PipelineInterface* pipeline, + std::shared_ptr playlistParser, + const std::string& url); + + void onPlaylistParsed( + std::string playlistUrl, + std::queue urls, + avsCommon::utils::playlistParser::PlaylistParseResult parseResult) override; + + bool hasAdditionalData() override; + bool handleEndOfStream() override; + + /** + * @note This function will block until the @c onPlaylistParsed callback. + * To avoid deadlock, callers must ensure that @c preprocess is not called on the same thread + * as the event loop handling @c onPlaylistParsed. + */ + void preprocess() override; + +private: + /** + * Constructor. + * + * @param pipeline The @c PipelineInterface through which the source of the @c AudioPipeline may be set. + * @param playlistParser The @c PlaylistParserInterface which will parse playlist urls. + * @param url The @c url from which to create the pipeline source from. + */ + UrlSource( + PipelineInterface* pipeline, + std::shared_ptr playlistParser, + const std::string& url); + + /** + * Initializes the UrlSource by doing the following: + * + * -# Attempt to parse the url as a playlist. + * -# Initialize internal url queue. + * -# Create the source element for the audio pipeline. + * -# Add the source element to the audio pipeline @c m_pipeline. + */ + bool init(); + + /// The url to read audioData from. + std::string m_url; + + /// A queue of parsed audio urls. This should not contain any playlist urls. + std::queue m_audioUrlQueue; + + /// A Playlist Parser. + std::shared_ptr m_playlistParser; + + /// Promise to notify when playlist parsing is complete. + std::promise> m_playlistParsedPromise; + + /// The @c PipelineInterface through which the source of the @c AudioPipeline may be set. + PipelineInterface* m_pipeline; +}; + +} // namespace mediaPlayer +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_MEDIA_PLAYER_INCLUDE_MEDIA_PLAYER_URL_SOURCE_H_ diff --git a/MediaPlayer/inputs/fox_dog_playlist.m3u b/MediaPlayer/inputs/fox_dog_playlist.m3u new file mode 100644 index 0000000000..eeaa5349dd --- /dev/null +++ b/MediaPlayer/inputs/fox_dog_playlist.m3u @@ -0,0 +1,5 @@ +#EXTM3U +#EXTINF:2,fox_dog.mp3 +fox_dog.mp3 +#EXTINF:2,fox_dog.mp3 +fox_dog.mp3 diff --git a/MediaPlayer/src/AttachmentReaderSource.cpp b/MediaPlayer/src/AttachmentReaderSource.cpp index 60288059eb..8503b08604 100644 --- a/MediaPlayer/src/AttachmentReaderSource.cpp +++ b/MediaPlayer/src/AttachmentReaderSource.cpp @@ -44,8 +44,8 @@ static const unsigned int CHUNK_SIZE(4096); std::unique_ptr AttachmentReaderSource::create( PipelineInterface* pipeline, - std::unique_ptr attachmentReader) { - std::unique_ptr result(new AttachmentReaderSource(pipeline, std::move(attachmentReader))); + std::shared_ptr attachmentReader) { + std::unique_ptr result(new AttachmentReaderSource(pipeline, attachmentReader)); if (result->init()) { return result; } @@ -58,10 +58,10 @@ AttachmentReaderSource::~AttachmentReaderSource() { AttachmentReaderSource::AttachmentReaderSource( PipelineInterface* pipeline, - std::unique_ptr reader) + std::shared_ptr reader) : BaseStreamSource{pipeline}, - m_reader{std::move(reader)} { + m_reader{reader} { }; bool AttachmentReaderSource::isOpen() { diff --git a/MediaPlayer/src/BaseStreamSource.cpp b/MediaPlayer/src/BaseStreamSource.cpp index 69c22aad06..c3ae807c6d 100644 --- a/MediaPlayer/src/BaseStreamSource.cpp +++ b/MediaPlayer/src/BaseStreamSource.cpp @@ -47,10 +47,26 @@ BaseStreamSource::BaseStreamSource(PipelineInterface* pipeline) : m_sourceId{0}, m_sourceRetryCount{0}, m_handleNeedDataFunction{[this]() { return handleNeedData(); }}, - m_handleEnoughDataFunction{[this]() { return handleEnoughData(); }} { + m_handleEnoughDataFunction{[this]() { return handleEnoughData(); }}, + m_needDataHandlerId{0}, + m_enoughDataHandlerId{0}, + m_needDataCallbackId{0}, + m_enoughDataCallbackId{0} { } BaseStreamSource::~BaseStreamSource() { + ACSDK_DEBUG9(LX("~BaseStreamSource")); + g_signal_handler_disconnect(m_pipeline->getAppSrc(), m_needDataHandlerId); + g_signal_handler_disconnect(m_pipeline->getAppSrc(), m_enoughDataHandlerId); + { + std::lock_guard lock(m_callbackIdMutex); + if (m_needDataCallbackId && !g_source_remove(m_needDataCallbackId)) { + ACSDK_ERROR(LX("gSourceRemove failed for m_needDataCallbackId")); + } + if (m_enoughDataCallbackId && !g_source_remove(m_enoughDataCallbackId)) { + ACSDK_ERROR(LX("gSourceRemove failed for m_enoughDataCallbackId")); + } + } uninstallOnReadDataHandler(); } @@ -96,7 +112,8 @@ bool BaseStreamSource::init() { * When the appsrc needs data, it emits the signal need-data. Connect the need-data signal to the onNeedData * callback which handles pushing data to the appsrc element. */ - if (!g_signal_connect(appsrc, "need-data", G_CALLBACK(onNeedData), this)) { + m_needDataHandlerId = g_signal_connect(appsrc, "need-data", G_CALLBACK(onNeedData), this); + if (0 == m_needDataHandlerId) { ACSDK_ERROR(LX("initFailed").d("reason", "connectNeedDataSignalFailed")); return false; } @@ -104,7 +121,8 @@ bool BaseStreamSource::init() { * When the appsrc had enough data, it emits the signal enough-data. Connect the enough-data signal to the * onEnoughData callback which handles stopping the data push to the appsrc element. */ - if (!g_signal_connect(appsrc, "enough-data", G_CALLBACK(onEnoughData), this)) { + m_enoughDataHandlerId = g_signal_connect(appsrc, "enough-data", G_CALLBACK(onEnoughData), this); + if (0 == m_enoughDataHandlerId) { ACSDK_ERROR(LX("initFailed").d("reason", "connectEnoughDataSignalFailed")); return false; } @@ -180,7 +198,7 @@ void BaseStreamSource::uninstallOnReadDataHandler() { } void BaseStreamSource::clearOnReadDataHandler() { - ACSDK_DEBUG9(LX("clearOnReadDataHandlerCalled")); + ACSDK_DEBUG9(LX("clearOnReadDataHandlerCalled").d("sourceId", m_sourceId)); m_sourceRetryCount = 0; m_sourceId = 0; } @@ -188,11 +206,18 @@ void BaseStreamSource::clearOnReadDataHandler() { void BaseStreamSource::onNeedData(GstElement *pipeline, guint size, gpointer pointer) { ACSDK_DEBUG9(LX("onNeedDataCalled").d("size", size)); auto source = static_cast(pointer); - source->m_pipeline->queueCallback(&source->m_handleNeedDataFunction); + std::lock_guard lock(source->m_callbackIdMutex); + if (source->m_needDataCallbackId) { + ACSDK_DEBUG9(LX("m_needDataCallbackId already set")); + return; + } + source->m_needDataCallbackId = source->m_pipeline->queueCallback(&source->m_handleNeedDataFunction); } gboolean BaseStreamSource::handleNeedData() { ACSDK_DEBUG9(LX("handleNeedDataCalled")); + std::lock_guard lock(m_callbackIdMutex); + m_needDataCallbackId = 0; installOnReadDataHandler(); return false; } @@ -200,11 +225,18 @@ gboolean BaseStreamSource::handleNeedData() { void BaseStreamSource::onEnoughData(GstElement *pipeline, gpointer pointer) { ACSDK_DEBUG9(LX("onEnoughDataCalled")); auto source = static_cast(pointer); - source->m_pipeline->queueCallback(&source->m_handleEnoughDataFunction); + std::lock_guard lock(source->m_callbackIdMutex); + if (source->m_enoughDataCallbackId) { + ACSDK_DEBUG9(LX("m_enoughDataCallbackId already set")); + return; + } + source->m_enoughDataCallbackId = source->m_pipeline->queueCallback(&source->m_handleEnoughDataFunction); } gboolean BaseStreamSource::handleEnoughData() { ACSDK_DEBUG9(LX("handleEnoughDataCalled")); + std::lock_guard lock(m_callbackIdMutex); + m_enoughDataCallbackId = 0; uninstallOnReadDataHandler(); return false; } @@ -213,5 +245,18 @@ gboolean BaseStreamSource::onReadData(gpointer pointer) { return static_cast(pointer)->handleReadData(); } +// No additional processing is necessary. +bool BaseStreamSource::handleEndOfStream() { + return true; +} + +// Source streams do not contain additional data. +bool BaseStreamSource::hasAdditionalData() { + return false; +} + +void BaseStreamSource::preprocess() { +} + } // namespace mediaPlayer } // namespace alexaClientSDK diff --git a/MediaPlayer/src/CMakeLists.txt b/MediaPlayer/src/CMakeLists.txt index e5b951cda2..600f81aa12 100644 --- a/MediaPlayer/src/CMakeLists.txt +++ b/MediaPlayer/src/CMakeLists.txt @@ -3,10 +3,12 @@ add_library(MediaPlayer SHARED AttachmentReaderSource.cpp BaseStreamSource.cpp IStreamSource.cpp - MediaPlayer.cpp) + MediaPlayer.cpp + UrlSource.cpp) target_include_directories(MediaPlayer PUBLIC "${MediaPlayer_SOURCE_DIR}/include" - "${GST_INCLUDE_DIRS}") + "${GST_INCLUDE_DIRS}" + "${PlaylistParser_SOURCE_DIR}/include") -target_link_libraries(MediaPlayer "${GST_LDFLAGS}" AVSCommon) +target_link_libraries(MediaPlayer "${GST_LDFLAGS}" AVSCommon PlaylistParser) diff --git a/MediaPlayer/src/IStreamSource.cpp b/MediaPlayer/src/IStreamSource.cpp index b861be0959..5b5982f450 100644 --- a/MediaPlayer/src/IStreamSource.cpp +++ b/MediaPlayer/src/IStreamSource.cpp @@ -44,7 +44,7 @@ static const unsigned int CHUNK_SIZE(4096); std::unique_ptr IStreamSource::create( PipelineInterface* pipeline, - std::unique_ptr stream, + std::shared_ptr stream, bool repeat) { std::unique_ptr result(new IStreamSource(pipeline, std::move(stream), repeat)); if (result->init()) { @@ -53,10 +53,10 @@ std::unique_ptr IStreamSource::create( return nullptr; }; -IStreamSource::IStreamSource(PipelineInterface* pipeline, std::unique_ptr stream, bool repeat) +IStreamSource::IStreamSource(PipelineInterface* pipeline, std::shared_ptr stream, bool repeat) : BaseStreamSource{pipeline}, - m_stream{std::move(stream)}, + m_stream{stream}, m_repeat{repeat} { }; @@ -103,7 +103,7 @@ gboolean IStreamSource::handleReadData() { m_stream->read(reinterpret_cast(info.data), info.size); - std::streamsize size; + unsigned long size; if (m_stream->bad()) { size = 0; ACSDK_WARN(LX("readFailed").d("bad", m_stream->bad()).d("eof", m_stream->eof())); diff --git a/MediaPlayer/src/MediaPlayer.cpp b/MediaPlayer/src/MediaPlayer.cpp index 651e00d450..2a97d55b4b 100644 --- a/MediaPlayer/src/MediaPlayer.cpp +++ b/MediaPlayer/src/MediaPlayer.cpp @@ -19,9 +19,15 @@ #include #include +#ifdef TOTEM_PLPARSER +#include +#else +#include +#endif #include "MediaPlayer/AttachmentReaderSource.h" #include "MediaPlayer/IStreamSource.h" +#include "MediaPlayer/UrlSource.h" #include "MediaPlayer/MediaPlayer.h" namespace alexaClientSDK { @@ -60,17 +66,22 @@ std::shared_ptr MediaPlayer::create() { MediaPlayer::~MediaPlayer() { ACSDK_DEBUG9(LX("~MediaPlayerCalled")); stop(); + // Destroy before g_main_loop. + if (m_setSourceThread.joinable()) { + m_setSourceThread.join(); + } g_main_loop_quit(m_mainLoop); if (m_mainLoopThread.joinable()) { m_mainLoopThread.join(); } gst_object_unref(m_pipeline.pipeline); + resetPipeline(); g_source_remove(m_busWatchId); g_main_loop_unref(m_mainLoop); } MediaPlayerStatus MediaPlayer::setSource( - std::unique_ptr reader) { + std::shared_ptr reader) { ACSDK_DEBUG9(LX("setSourceCalled").d("sourceType", "AttachmentReader")); std::promise promise; auto future = promise.get_future(); @@ -82,20 +93,46 @@ MediaPlayerStatus MediaPlayer::setSource( return future.get(); } -MediaPlayerStatus MediaPlayer::setSource(std::unique_ptr stream, bool repeat) { +MediaPlayerStatus MediaPlayer::setSource(std::shared_ptr stream, bool repeat) { ACSDK_DEBUG9(LX("setSourceCalled").d("sourceType", "istream")); std::promise promise; auto future = promise.get_future(); std::function callback = [this, &promise, &stream, repeat]() { - handleSetIStreamSource(&promise, std::move(stream), repeat); + handleSetIStreamSource(&promise, stream, repeat); return false; }; queueCallback(&callback); return future.get(); } +MediaPlayerStatus MediaPlayer::setSource(const std::string& url) { + ACSDK_DEBUG9(LX("setSourceForUrlCalled")); + std::promise promise; + std::future future = promise.get_future(); + /* + * A separate thread is needed because the UrlSource needs block and wait for callbacks + * from the main event loop (g_main_loop). Deadlock will occur if UrlSource is created + * on the main event loop. + */ + if (m_setSourceThread.joinable()) { + m_setSourceThread.join(); + } + m_setSourceThread = std::thread( + &MediaPlayer::handleSetSource, + this, + std::move(promise), url); + + return future.get(); +} + MediaPlayerStatus MediaPlayer::play() { ACSDK_DEBUG9(LX("playCalled")); + if (!m_source) { + ACSDK_ERROR(LX("playFailed").d("reason", "sourceNotSet")); + return MediaPlayerStatus::FAILURE; + } + m_source->preprocess(); + std::promise promise; auto future = promise.get_future(); std::function callback = [this, &promise]() { @@ -118,6 +155,30 @@ MediaPlayerStatus MediaPlayer::stop() { return future.get(); } +MediaPlayerStatus MediaPlayer::pause() { + ACSDK_DEBUG9(LX("pausedCalled")); + std::promise promise; + auto future = promise.get_future(); + std::function callback = [this, &promise]() { + handlePause(&promise); + return false; + }; + queueCallback(&callback); + return future.get(); +} + +MediaPlayerStatus MediaPlayer::resume() { + ACSDK_DEBUG9(LX("resumeCalled")); + std::promise promise; + auto future = promise.get_future(); + std::function callback = [this, &promise]() { + handleResume(&promise); + return false; + }; + queueCallback(&callback); + return future.get(); +} + int64_t MediaPlayer::getOffsetInMilliseconds() { ACSDK_DEBUG9(LX("getOffsetInMillisecondsCalled")); std::promise promise; @@ -163,8 +224,10 @@ GstElement* MediaPlayer::getPipeline() const { } MediaPlayer::MediaPlayer() : - m_pipeline{nullptr, nullptr, nullptr, nullptr, nullptr}, + m_playbackStartedSent{false}, m_playbackFinishedSent{false}, + m_isPaused{false}, + m_isBufferUnderrun{false}, m_playerObserver{nullptr} { } @@ -220,16 +283,26 @@ bool MediaPlayer::setupPipeline() { } void MediaPlayer::tearDownPipeline() { + ACSDK_DEBUG9(LX("tearDownPipeline")); if (m_pipeline.pipeline) { doStop(); gst_object_unref(m_pipeline.pipeline); - m_pipeline.pipeline = nullptr; + resetPipeline(); g_source_remove(m_busWatchId); } } -void MediaPlayer::queueCallback(const std::function *callback) { - g_idle_add(reinterpret_cast(&onCallback), const_cast *>(callback)); +void MediaPlayer::resetPipeline() { + ACSDK_DEBUG9(LX("resetPipeline")); + m_pipeline.pipeline = nullptr; + m_pipeline.appsrc = nullptr; + m_pipeline.decoder = nullptr; + m_pipeline.converter = nullptr; + m_pipeline.audioSink = nullptr; +} + +guint MediaPlayer::queueCallback(const std::function *callback) { + return g_idle_add(reinterpret_cast(&onCallback), const_cast *>(callback)); } gboolean MediaPlayer::onCallback(const std::function *callback) { @@ -264,17 +337,49 @@ gboolean MediaPlayer::handleBusMessage(GstMessage *message) { switch (GST_MESSAGE_TYPE(message)) { case GST_MESSAGE_EOS: if (GST_MESSAGE_SRC(message) == GST_OBJECT_CAST(m_pipeline.pipeline)) { - sendPlaybackFinished(); + if (!m_source->handleEndOfStream()) { + alexaClientSDK::avsCommon::utils::logger::LogEntry *errorDescription = + &(LX("handleBusMessageFailed").d("reason", "sourceHandleEndOfStreamFailed")); + ACSDK_ERROR(*errorDescription); + sendPlaybackError(errorDescription->c_str()); + } + + // Continue playback if there is additional data. + if (m_source->hasAdditionalData()) { + if (GST_STATE_CHANGE_FAILURE == gst_element_set_state(m_pipeline.pipeline, GST_STATE_NULL)) { + alexaClientSDK::avsCommon::utils::logger::LogEntry *errorDescription = + &(LX("continuingPlaybackFailed").d("reason", "setPiplineToNullFailed")); + + ACSDK_ERROR(*errorDescription); + sendPlaybackError(errorDescription->c_str()); + } + + if (GST_STATE_CHANGE_FAILURE == gst_element_set_state(m_pipeline.pipeline, GST_STATE_PLAYING)) { + alexaClientSDK::avsCommon::utils::logger::LogEntry *errorDescription = + &(LX("continuingPlaybackFailed").d("reason", "setPiplineToPlayingFailed")); + + ACSDK_ERROR(*errorDescription); + sendPlaybackError(errorDescription->c_str()); + } + } else { + sendPlaybackFinished(); + } } break; case GST_MESSAGE_ERROR: { GError *error; - gst_message_parse_error(message, &error, nullptr); + gchar *debug; + gst_message_parse_error(message, &error, &debug); + std::string messageSrcName = GST_MESSAGE_SRC_NAME(message); - ACSDK_ERROR(LX("handleBusMessageError").d("source", messageSrcName).d("error", error->message)); + ACSDK_ERROR(LX("handleBusMessageError") + .d("source", messageSrcName) + .d("error", error->message) + .d("debug", debug ? debug : "noInfo")); sendPlaybackError(error->message); g_error_free(error); + g_free(debug); break; } case GST_MESSAGE_STATE_CHANGED: { @@ -285,18 +390,56 @@ gboolean MediaPlayer::handleBusMessage(GstMessage *message) { GstState pendingState; gst_message_parse_state_changed(message, &oldState, &newState, &pendingState); if (newState == GST_STATE_PLAYING) { - sendPlaybackStarted(); - } - /* - * If the previous state was PLAYING and the new state is PAUSED, ie, the audio has stopped playing, - * indicate to the observer that playback has finished. - */ - if (newState == GST_STATE_PAUSED && oldState == GST_STATE_PLAYING) { + if (!m_playbackStartedSent) { + sendPlaybackStarted(); + } else { + if (m_isBufferUnderrun) { + sendBufferRefilled(); + m_isBufferUnderrun = false; + } else if (m_isPaused) { + sendPlaybackResumed(); + m_isPaused = false; + } + } + } else if (newState == GST_STATE_PAUSED && + oldState == GST_STATE_PLAYING) { + if (m_isBufferUnderrun) { + sendBufferUnderrun(); + } else if (!m_isPaused) { + sendPlaybackPaused(); + m_isPaused = true; + } + } else if (newState == GST_STATE_NULL && oldState == GST_STATE_READY) { sendPlaybackFinished(); } } break; } + case GST_MESSAGE_BUFFERING: { + gint bufferPercent = 0; + gst_message_parse_buffering(message, &bufferPercent); + ACSDK_DEBUG9(LX("handleBusMessage").d("message", "GST_MESSAGE_BUFFERING").d("percent", bufferPercent)); + + if (bufferPercent < 100) { + if (GST_STATE_CHANGE_FAILURE == gst_element_set_state(m_pipeline.pipeline, GST_STATE_PAUSED)) { + std::string error = "pausingOnBufferUnderrunFailed"; + ACSDK_ERROR(LX(error)); + sendPlaybackError(error); + break; + } + // Only enter bufferUnderrun after playback has started. + if (m_playbackStartedSent) { + m_isBufferUnderrun = true; + } + } else { + if (GST_STATE_CHANGE_FAILURE == gst_element_set_state(m_pipeline.pipeline, GST_STATE_PLAYING)) { + std::string error = "resumingOnBufferRefilledFailed"; + ACSDK_ERROR(LX(error)); + sendPlaybackError(error); + } + } + break; + } default: break; } @@ -305,7 +448,7 @@ gboolean MediaPlayer::handleBusMessage(GstMessage *message) { } void MediaPlayer::handleSetAttachmentReaderSource( - std::promise *promise, std::unique_ptr reader) { + std::promise *promise, std::shared_ptr reader) { ACSDK_DEBUG(LX("handleSetSourceCalled")); tearDownPipeline(); @@ -316,7 +459,7 @@ void MediaPlayer::handleSetAttachmentReaderSource( return; } - m_source = AttachmentReaderSource::create(this, std::move(reader)); + m_source = std::move(AttachmentReaderSource::create(this, reader)); if (!m_source) { ACSDK_ERROR(LX("handleSetAttachmentReaderSourceFailed").d("reason", "sourceIsNullptr")); @@ -338,7 +481,7 @@ void MediaPlayer::handleSetAttachmentReaderSource( } void MediaPlayer::handleSetIStreamSource( - std::promise *promise, std::unique_ptr stream, bool repeat) { + std::promise *promise, std::shared_ptr stream, bool repeat) { ACSDK_DEBUG(LX("handleSetSourceCalled")); tearDownPipeline(); @@ -349,7 +492,7 @@ void MediaPlayer::handleSetIStreamSource( return; } - m_source = IStreamSource::create(this, std::move(stream), repeat); + m_source = std::move(IStreamSource::create(this, stream, repeat)); if (!m_source) { ACSDK_ERROR(LX("handleSetIStreamSourceFailed").d("reason", "sourceIsNullptr")); @@ -370,14 +513,48 @@ void MediaPlayer::handleSetIStreamSource( promise->set_value(MediaPlayerStatus::SUCCESS); } -void MediaPlayer::handlePlay(std::promise *promise) { - ACSDK_DEBUG(LX("handlePlayCalled")); +void MediaPlayer::handleSetSource(std::promise promise, std::string url) { + ACSDK_DEBUG(LX("handleSetSourceForUrlCalled")); + + tearDownPipeline(); + + if (!setupPipeline()) { + ACSDK_ERROR(LX("handleSetSourceForUrlFailed").d("reason", "setupPipelineFailed")); + promise.set_value(MediaPlayerStatus::FAILURE); + return; + } + +#ifdef TOTEM_PLPARSER + m_source = UrlSource::create(this, alexaClientSDK::playlistParser::PlaylistParser::create(), url); +#else + m_source = UrlSource::create(this, alexaClientSDK::playlistParser::DummyPlaylistParser::create(), url); +#endif + if (!m_source) { - ACSDK_ERROR(LX("handlePlayFailed").d("reason", "sourceNotSet")); - promise->set_value(MediaPlayerStatus::FAILURE); + ACSDK_ERROR(LX("handleSetSourceForUrlFailed").d("reason", "sourceIsNullptr")); + promise.set_value(MediaPlayerStatus::FAILURE); + return; + } + + /* + * This works with audio only sources. This does not work for any source that has more than one stream. + * The first pad that is added may not be the correct stream (ie may be a video stream), and will fail. + * + * Once the source pad for the decoder has been added, the decoder emits the pad-added signal. Connect the signal + * to the callback which performs the linking of the decoder source pad to the converter sink pad. + */ + if (!g_signal_connect(m_pipeline.decoder, "pad-added", G_CALLBACK(onPadAdded), this)) { + ACSDK_ERROR(LX("handleSetSourceForUrlFailed").d("reason", "connectPadAddedSignalFailed")); + promise.set_value(MediaPlayerStatus::FAILURE); return; } + promise.set_value(MediaPlayerStatus::SUCCESS); +} + +void MediaPlayer::handlePlay(std::promise *promise) { + ACSDK_DEBUG(LX("handlePlayCalled")); + // If the player was in PLAYING state or was pending transition to PLAYING state, stop playing audio. if (MediaPlayerStatus::SUCCESS != doStop()) { ACSDK_ERROR(LX("handlePlayFailed").d("reason", "doStopFailed")); @@ -388,6 +565,7 @@ void MediaPlayer::handlePlay(std::promise *promise) { m_playbackFinishedSent = false; auto stateChangeRet = gst_element_set_state(m_pipeline.pipeline, GST_STATE_PLAYING); + ACSDK_DEBUG(LX("handlePlay").d("stateReturn", gst_element_state_change_return_get_name(stateChangeRet))); if (GST_STATE_CHANGE_FAILURE == stateChangeRet) { ACSDK_ERROR(LX("handlePlayFailed").d("reason", "gstElementSetStateFailure")); promise->set_value(MediaPlayerStatus::FAILURE); @@ -436,21 +614,98 @@ MediaPlayerStatus MediaPlayer::doStop() { return MediaPlayerStatus::SUCCESS; } +void MediaPlayer::handlePause(std::promise *promise) { + ACSDK_DEBUG(LX("handlePauseCalled")); + if (!m_source) { + ACSDK_ERROR(LX("handlePauseFailed").d("reason", "sourceNotSet")); + promise->set_value(MediaPlayerStatus::FAILURE); + return; + } + + GstState curState; + // If previous set state return was GST_STATE_CHANGE_ASYNC, this will block infinitely + // until that state has been set. + auto stateChangeRet = gst_element_get_state(m_pipeline.pipeline, &curState, NULL, GST_CLOCK_TIME_NONE); + if (GST_STATE_CHANGE_FAILURE == stateChangeRet) { + ACSDK_ERROR(LX("handlePauseFailed").d("reason", "gstElementGetStateFailure")); + promise->set_value(MediaPlayerStatus::FAILURE); + return; + } + + // Error if attempting to pause in any other state. + if (curState != GST_STATE_PLAYING) { + ACSDK_ERROR(LX("handlePauseFailed").d("reason", "noAudioPlaying")); + promise->set_value(MediaPlayerStatus::FAILURE); + return; + } + + stateChangeRet = gst_element_set_state(m_pipeline.pipeline, GST_STATE_PAUSED); + if (GST_STATE_CHANGE_FAILURE == stateChangeRet) { + ACSDK_ERROR(LX("handlePauseFailed").d("reason", "gstElementSetStateFailure")); + promise->set_value(MediaPlayerStatus::FAILURE); + } else if (GST_STATE_CHANGE_ASYNC == stateChangeRet) { + promise->set_value(MediaPlayerStatus::PENDING); + } else { + promise->set_value(MediaPlayerStatus::SUCCESS); + } + return; +} + +void MediaPlayer::handleResume(std::promise *promise) { + ACSDK_DEBUG(LX("handleResumeCalled")); + if (!m_source) { + ACSDK_ERROR(LX("handleResumeFailed").d("reason", "sourceNotSet")); + promise->set_value(MediaPlayerStatus::FAILURE); + return; + } + + GstState curState; + // If previous set state return was GST_STATE_CHANGE_ASYNC, this will block infinitely + // until that state has been set. + auto stateChangeRet = gst_element_get_state(m_pipeline.pipeline, &curState, NULL, GST_CLOCK_TIME_NONE); + + if (GST_STATE_CHANGE_FAILURE == stateChangeRet) { + ACSDK_ERROR(LX("handleResumeFailed").d("reason", "gstElementGetStateFailure")); + promise->set_value(MediaPlayerStatus::FAILURE); + } + + // Only unpause if currently paused. + if (curState != GST_STATE_PAUSED) { + ACSDK_ERROR(LX("handleResumeFailed").d("reason", "notCurrentlyPaused")); + promise->set_value(MediaPlayerStatus::FAILURE); + return; + } + + stateChangeRet = gst_element_set_state(m_pipeline.pipeline, GST_STATE_PLAYING); + if (GST_STATE_CHANGE_FAILURE == stateChangeRet) { + ACSDK_ERROR(LX("handleResumeFailed").d("reason", "gstElementSetStateFailure")); + promise->set_value(MediaPlayerStatus::FAILURE); + } else if (GST_STATE_CHANGE_ASYNC == stateChangeRet) { + promise->set_value(MediaPlayerStatus::PENDING); + } else { + promise->set_value(MediaPlayerStatus::SUCCESS); + } + + return; +} + void MediaPlayer::handleGetOffsetInMilliseconds(std::promise *promise) { ACSDK_DEBUG(LX("handleGetOffsetInMillisecondsCalled")); gint64 position = -1; GstState state; GstState pending; - - auto stateChangeRet = gst_element_get_state( - m_pipeline.pipeline, &state, &pending, TIMEOUT_ZERO_NANOSECONDS); - if (GST_STATE_CHANGE_FAILURE == stateChangeRet) { - ACSDK_ERROR(LX("handleGetOffsetInMillisecondsFailed").d("reason", "getElementGetStateFailed")); - } else if (GST_STATE_CHANGE_SUCCESS == stateChangeRet && - (GST_STATE_PLAYING == state || GST_STATE_PAUSED == state) && - gst_element_query_position(m_pipeline.pipeline, GST_FORMAT_TIME, &position)) { - position /= NANOSECONDS_TO_MILLISECONDS; + if (m_pipeline.pipeline) { + auto stateChangeRet = gst_element_get_state( + m_pipeline.pipeline, &state, &pending, TIMEOUT_ZERO_NANOSECONDS); + if (GST_STATE_CHANGE_FAILURE == stateChangeRet) { + ACSDK_ERROR(LX("handleGetOffsetInMillisecondsFailed").d("reason", "getElementGetStateFailed")); + } else if (GST_STATE_CHANGE_SUCCESS == stateChangeRet && + (GST_STATE_PLAYING == state || GST_STATE_PAUSED == state) && + gst_element_query_position(m_pipeline.pipeline, GST_FORMAT_TIME, &position)) { + position /= NANOSECONDS_TO_MILLISECONDS; + } } + promise->set_value(static_cast(position)); } @@ -463,14 +718,19 @@ void MediaPlayer::handleSetObserver( } void MediaPlayer::sendPlaybackStarted() { - ACSDK_DEBUG(LX("callingOnPlaybackStarted")); - if (m_playerObserver) { - m_playerObserver->onPlaybackStarted(); + if (!m_playbackStartedSent) { + ACSDK_DEBUG(LX("callingOnPlaybackStarted")); + m_playbackStartedSent = true; + if (m_playerObserver) { + m_playerObserver->onPlaybackStarted(); + } } } void MediaPlayer::sendPlaybackFinished() { m_source.reset(); + m_isPaused = false; + m_playbackStartedSent = false; if (!m_playbackFinishedSent) { m_playbackFinishedSent = true; ACSDK_DEBUG(LX("callingOnPlaybackFinished")); @@ -480,6 +740,20 @@ void MediaPlayer::sendPlaybackFinished() { } } +void MediaPlayer::sendPlaybackPaused() { + ACSDK_DEBUG(LX("callingOnPlaybackPaused")); + if (m_playerObserver) { + m_playerObserver->onPlaybackPaused(); + } +} + +void MediaPlayer::sendPlaybackResumed() { + ACSDK_DEBUG(LX("callingOnPlaybackResumed")); + if (m_playerObserver) { + m_playerObserver->onPlaybackResumed(); + } +} + void MediaPlayer::sendPlaybackError(const std::string& error) { ACSDK_DEBUG(LX("callingOnPlaybackError").d("error", error)); if (m_playerObserver) { @@ -487,5 +761,19 @@ void MediaPlayer::sendPlaybackError(const std::string& error) { } } +void MediaPlayer::sendBufferUnderrun() { + ACSDK_DEBUG(LX("callingOnBufferUnderrun")); + if (m_playerObserver) { + m_playerObserver->onBufferUnderrun(); + } +} + +void MediaPlayer::sendBufferRefilled() { + ACSDK_DEBUG(LX("callingOnBufferRefilled")); + if (m_playerObserver) { + m_playerObserver->onBufferRefilled(); + } +} + } // namespace mediaPlayer } // namespace alexaClientSDK diff --git a/MediaPlayer/src/UrlSource.cpp b/MediaPlayer/src/UrlSource.cpp new file mode 100644 index 0000000000..a63130b261 --- /dev/null +++ b/MediaPlayer/src/UrlSource.cpp @@ -0,0 +1,151 @@ +/* + * UrlSource.cpp + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 "MediaPlayer/UrlSource.h" + +namespace alexaClientSDK { +namespace mediaPlayer { + +using namespace avsCommon::utils; +using namespace avsCommon::utils::mediaPlayer; +using namespace avsCommon::avs::attachment; +using namespace avsCommon::utils::playlistParser; + +/// String to identify log entries originating from this file. +static const std::string TAG("UrlSource"); + +/** + * Create a LogEntry using this file's TAG and the specified event string. + * + * @param The event string for this @c LogEntry. + */ +#define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) + +std::shared_ptr UrlSource::create( + PipelineInterface* pipeline, + std::shared_ptr playlistParser, + const std::string& url) { + if (!pipeline) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullPipeline")); + return nullptr; + } + if (!playlistParser) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullPlaylistParser")); + return nullptr; + } + + std::shared_ptr result(new UrlSource(pipeline, playlistParser, url)); + if (result->init()) { + return result; + } + return nullptr; +} + +UrlSource::UrlSource( + PipelineInterface* pipeline, + std::shared_ptr playlistParser, + const std::string& url) : + m_url{url}, + m_playlistParser{playlistParser}, + m_pipeline{pipeline} { +} + +bool UrlSource::init() { + ACSDK_DEBUG(LX("initCalledForUrlSource")); + + m_playlistParser->parsePlaylist(m_url, shared_from_this()); + + auto decoder = gst_element_factory_make("uridecodebin", "decoder"); + if (!decoder) { + ACSDK_ERROR(LX("initFailed").d("reason", "createDecoderElementFailed")); + return false; + } + + if (!gst_bin_add(GST_BIN(m_pipeline->getPipeline()), decoder)) { + ACSDK_ERROR(LX("initFailed").d("reason", "addingDecoderToPipelineFailed")); + gst_object_unref(decoder); + return false; + } + + m_pipeline->setAppSrc(nullptr); + m_pipeline->setDecoder(decoder); + + return true; +} + +bool UrlSource::hasAdditionalData() { + return !m_url.empty(); +} + +bool UrlSource::handleEndOfStream() { + m_url.clear(); + // TODO [ACSDK-419] Solidify contract that the parsed Urls will not be empty. + while (!m_audioUrlQueue.empty()) { + std::string url = m_audioUrlQueue.front(); + m_audioUrlQueue.pop(); + if (url.empty()) { + ACSDK_INFO(LX("handleEndOfStream").d("info", "emptyUrlFound")); + continue; + } + m_url = url; + } + + if (!m_url.empty()) { + g_object_set(m_pipeline->getDecoder(), "uri", m_url.c_str(), NULL); + } + return true; +} + +void UrlSource::preprocess() { + m_audioUrlQueue = m_playlistParsedPromise.get_future().get(); + /* + * TODO: Reset the playlistParser in a better place once the thread model of MediaPlayer + * is simplified [ACSDK-422]. + * + * This must be called from a thread not in the UrlSource/PlaylistParser loop + * to prevent a thread from joining itself. + */ + m_playlistParser.reset(); + + if (m_audioUrlQueue.empty()) { + ACSDK_ERROR(LX("preprocess").d("reason", "noValidUrls")); + return; + } + m_url = m_audioUrlQueue.front(); + m_audioUrlQueue.pop(); + + g_object_set(m_pipeline->getDecoder(), + "uri", m_url.c_str(), + "use-buffering", true, + NULL); +} + +void UrlSource::onPlaylistParsed(std::string initialUrl, std::queue urls, PlaylistParseResult parseResult) { + ACSDK_DEBUG(LX("onPlaylistParsed").d("parseResult", parseResult).d("numUrlsParsed", urls.size())); + // The parse was unrecognized by the parser, could be a single song. Attempt to play. + if (urls.size() == 0) { + urls.push(initialUrl); + } + m_playlistParsedPromise.set_value(urls); +}; + +} // namespace mediaPlayer +} // namespace alexaClientSDK diff --git a/MediaPlayer/test/MediaPlayerTest.cpp b/MediaPlayer/test/MediaPlayerTest.cpp index 4da5baafe9..c9fda7f121 100644 --- a/MediaPlayer/test/MediaPlayerTest.cpp +++ b/MediaPlayer/test/MediaPlayerTest.cpp @@ -56,6 +56,12 @@ std::string inputsDirPath; /// MP3 test file path. static const std::string MP3_FILE_PATH("/fox_dog.mp3"); +/// Playlist file path. +static const std::string M3U_FILE_PATH("/fox_dog_playlist.m3u"); + +/// file URI Prefix +static const std::string FILE_PREFIX("file://"); + /** * Mock AttachmentReader. */ @@ -197,6 +203,10 @@ class MockPlayerObserver: public MediaPlayerObserverInterface { void onPlaybackError(std::string error) override; + void onPlaybackPaused() override; + + void onPlaybackResumed() override; + /** * Wait for a message to be received. * @@ -215,6 +225,38 @@ class MockPlayerObserver: public MediaPlayerObserverInterface { */ bool waitForPlaybackFinished(const std::chrono::milliseconds duration = std::chrono::milliseconds(5000)); + /** + * Wait for a message to be received. + * + * This function waits for a specified number of milliseconds for a message to arrive. + * @param duration Number of milliseconds to wait before giving up. + * @return true if a message was received within the specified duration, else false. + */ + bool waitForPlaybackPaused(const std::chrono::milliseconds duration = std::chrono::milliseconds(5000)); + + /** + * Wait for a message to be received. + * + * This function waits for a specified number of milliseconds for a message to arrive. + * @param duration Number of milliseconds to wait before giving up. + * @return true if a message was received within the specified duration, else false. + */ + bool waitForPlaybackResumed(const std::chrono::milliseconds duration = std::chrono::milliseconds(5000)); + + /** + * TODO: Make this class a mock and remove this. + * + * This gets the number of times onPlaybackStarted was called. + */ + int getOnPlaybackStartedCallCount(); + + /** + * TODO: Make this class a mock and remove this. + * + * This gets the number of times onPlaybackFinished was called. + */ + int getOnPlaybackFinishedCallCount(); + private: /// Mutex to protect the flags @c m_playbackStarted and .@c m_playbackFinished. std::mutex m_mutex; @@ -222,10 +264,23 @@ class MockPlayerObserver: public MediaPlayerObserverInterface { std::condition_variable m_wakePlaybackStarted; /// Trigger to wake up m_wakePlaybackStarted calls. std::condition_variable m_wakePlaybackFinished; + /// Trigger to wake up m_wakePlaybackPaused calls. + std::condition_variable m_wakePlaybackPaused; + /// Trigger to wake up m_wakePlaybackResumed calls. + std::condition_variable m_wakePlaybackResumed; + + // TODO: Make this class a mock and remove these. + int m_onPlaybackStartedCallCount = 0; + int m_onPlaybackFinishedCallCount = 0; + /// Flag to set when a playback start message is received. bool m_playbackStarted; /// Flag to set when a playback finished message is received. bool m_playbackFinished; + /// Flag to set when a playback paused message is received. + bool m_playbackPaused; + /// Flag to set when a playback paused message is received. + bool m_playbackResumed; }; void MockPlayerObserver::onPlaybackStarted() { @@ -233,6 +288,7 @@ void MockPlayerObserver::onPlaybackStarted() { m_playbackStarted = true; m_playbackFinished = false; m_wakePlaybackStarted.notify_all(); + m_onPlaybackStartedCallCount++; } void MockPlayerObserver::onPlaybackFinished() { @@ -240,12 +296,26 @@ void MockPlayerObserver::onPlaybackFinished() { m_playbackFinished = true; m_playbackStarted = false; m_wakePlaybackFinished.notify_all(); + m_onPlaybackFinishedCallCount++; } void MockPlayerObserver::onPlaybackError(std::string error) { ACSDK_ERROR(LX("onPlaybackError").d("error", error)); }; +void MockPlayerObserver::onPlaybackPaused() { + std::lock_guard lock(m_mutex); + m_playbackPaused = true; + m_wakePlaybackPaused.notify_all(); +}; + +void MockPlayerObserver::onPlaybackResumed() { + std::lock_guard lock(m_mutex); + m_playbackResumed = true; + m_playbackPaused = false; + m_wakePlaybackResumed.notify_all(); +}; + bool MockPlayerObserver::waitForPlaybackStarted(const std::chrono::milliseconds duration) { std::unique_lock lock(m_mutex); if (!m_wakePlaybackStarted.wait_for(lock, duration, [this]() { return m_playbackStarted; } )) @@ -264,6 +334,32 @@ bool MockPlayerObserver::waitForPlaybackFinished(const std::chrono::milliseconds return true; } +bool MockPlayerObserver::waitForPlaybackPaused(const std::chrono::milliseconds duration) { + std::unique_lock lock(m_mutex); + if (!m_wakePlaybackPaused.wait_for(lock, duration, [this]() { return m_playbackPaused; } )) + { + return false; + } + return true; +} + +bool MockPlayerObserver::waitForPlaybackResumed(const std::chrono::milliseconds duration) { + std::unique_lock lock(m_mutex); + if (!m_wakePlaybackResumed.wait_for(lock, duration, [this]() { return m_playbackResumed; } )) + { + return false; + } + return true; +} + +int MockPlayerObserver::getOnPlaybackStartedCallCount() { + return m_onPlaybackStartedCallCount; +} + +int MockPlayerObserver::getOnPlaybackFinishedCallCount() { + return m_onPlaybackFinishedCallCount; +} + class MediaPlayerTest: public ::testing::Test{ public: void SetUp() override; @@ -319,6 +415,36 @@ TEST_F(MediaPlayerTest, testStartPlayWaitForEnd) { ASSERT_TRUE(m_playerObserver->waitForPlaybackFinished()); } +/** + * Set the source of the @c MediaPlayer to a url representing a single audio file. Playback audio till the end. + * Check whether the playback started and playback finished notifications are received. + */ +TEST_F(MediaPlayerTest, testStartPlayForUrl) { + + std::string url_single(FILE_PREFIX + inputsDirPath + MP3_FILE_PATH); + m_mediaPlayer->setSource(url_single); + + ASSERT_NE(MediaPlayerStatus::FAILURE,m_mediaPlayer->play()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackStarted()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackFinished()); +} + +/** + * Set the source of the @c MediaPlayer twice consecutively to a url representing a single audio file. + * Playback audio till the end. Check whether the playback started and playback finished notifications + * are received. + */ +TEST_F(MediaPlayerTest, testConsecutiveSetSource) { + + std::string url_single(FILE_PREFIX + inputsDirPath + MP3_FILE_PATH); + m_mediaPlayer->setSource(""); + m_mediaPlayer->setSource(url_single); + + ASSERT_NE(MediaPlayerStatus::FAILURE,m_mediaPlayer->play()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackStarted()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackFinished()); +} + /** * Read an audio file into a buffer. Set the source of the @c MediaPlayer to the buffer. Playback audio till the end. * Check whether the playback started and playback finished notifications are received. @@ -375,6 +501,123 @@ TEST_F(MediaPlayerTest, testStartPlayCallAfterStopPlay) { ASSERT_TRUE(m_playerObserver->waitForPlaybackFinished()); } +/* + * Pause an audio after playback has started. + */ +TEST_F(MediaPlayerTest, testPauseDuringPlay) { + setIStreamSource(true); + + ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->play()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackStarted()); + std::this_thread::sleep_for(std::chrono::seconds(1)); + ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->pause()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackPaused()); + // onPlaybackFinish should NOT be called during a pause. + // TODO: Detect this via making the MediaPlayerObserverMock a mock object. + ASSERT_EQ(m_playerObserver->getOnPlaybackFinishedCallCount(), 0); +} + +/* + * Play of a paused audio. This behavior is not supported, and will result in the + * audio being stopped. + */ +TEST_F(MediaPlayerTest, testPlayAfterPause) { + setIStreamSource(true); + + ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->play()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackStarted()); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + ASSERT_NE(MediaPlayerStatus::FAILURE,m_mediaPlayer->pause()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackPaused()); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->play()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackFinished()); + // TODO: Make the MediaPlayerObserverMock a mock object and ensure onPlaybackStarted should NOT be called + // during a resume. + ASSERT_EQ(m_playerObserver->getOnPlaybackStartedCallCount(), 1); + ASSERT_EQ(m_playerObserver->getOnPlaybackFinishedCallCount(), 1); +} + +/* + * Stop of a paused audio after playback has started. An additional stop and play event should + * be sent. + */ +TEST_F(MediaPlayerTest, testStopAfterPause) { + setIStreamSource(true); + + ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->play()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackStarted()); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->pause()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackPaused()); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->stop()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackFinished()); + // TODO: Make the MediaPlayerObserverMock a mock object and ensure onPlaybackStarted should NOT be called + // during a resume. + ASSERT_EQ(m_playerObserver->getOnPlaybackStartedCallCount(), 1); + ASSERT_EQ(m_playerObserver->getOnPlaybackFinishedCallCount(), 1); +} + +/* + * Pause of a paused audio after playback has started. The pause() should fail. + */ +TEST_F(MediaPlayerTest, testPauseAfterPause) { + setIStreamSource(true); + + ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->play()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackStarted()); + + ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->pause()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackPaused()); + + ASSERT_EQ(MediaPlayerStatus::FAILURE, m_mediaPlayer->pause()); + + ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->stop()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackFinished()); +} + +/* + * Resume play of a paused audio after playback has started. + */ +TEST_F(MediaPlayerTest, testResumeAfterPause) { + setIStreamSource(true); + + ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->play()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackStarted()); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->pause()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackPaused()); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->resume()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackResumed()); + // onPlaybackStarted should NOT be called during a pause. + // TODO: Make the MediaPlayerObserverMock a mock object and ensure onPlaybackStarted should NOT be called + // during a resume. + ASSERT_EQ(m_playerObserver->getOnPlaybackStartedCallCount(), 1); +} + +/* + * Calling resume after playback has started. The resume operation should fail. + */ +TEST_F(MediaPlayerTest, testResumeAfterPlay) { + setIStreamSource(true); + + ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->play()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackStarted()); + + ASSERT_EQ(MediaPlayerStatus::FAILURE, m_mediaPlayer->resume()); + + ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->stop()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackFinished()); +} + /** * Read an audio file into a buffer. Set the source of the @c MediaPlayer to the buffer. Playback audio for a few * seconds. Playback started notification should be received when the playback starts. Call @c getOffsetInMilliseconds. @@ -480,6 +723,25 @@ TEST_F(MediaPlayerTest, testRecoveryFromPausedReads) { #endif +#ifdef TOTEM_PLPARSER +/** + * Check playback of an URL identifying a playlist. Wait until the end. + * Ensure that onPlaybackStarted and onPlaybackFinished are only called once each. + */ + +TEST_F(MediaPlayerTest, testStartPlayWithUrlPlaylistWaitForEnd) { + + std::string url_playlist(FILE_PREFIX + inputsDirPath + M3U_FILE_PATH); + m_mediaPlayer->setSource(url_playlist); + + ASSERT_NE(MediaPlayerStatus::FAILURE,m_mediaPlayer->play()); + ASSERT_TRUE(m_playerObserver->waitForPlaybackStarted(std::chrono::milliseconds(10000))); + ASSERT_TRUE(m_playerObserver->waitForPlaybackFinished(std::chrono::milliseconds(10000))); + ASSERT_EQ(m_playerObserver->getOnPlaybackStartedCallCount(), 1); + ASSERT_EQ(m_playerObserver->getOnPlaybackFinishedCallCount(), 1); +} +#endif + } // namespace test } // namespace mediaPlayer } // namespace alexaClientSDK @@ -488,7 +750,7 @@ int main (int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); if (argc < 2) { - std::cerr << "Usage: MediaPlayerTest " << std::endl; + std::cerr << "Usage: MediaPlayerTest " << std::endl; } else { alexaClientSDK::mediaPlayer::test::inputsDirPath = std::string(argv[1]); return RUN_ALL_TESTS(); diff --git a/NOTICE.txt b/NOTICE.txt index f22108099d..617c641a1e 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,11 +1,11 @@ -Alexa Client SDK +AVS Device SDK Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. *************************** -ALEXA CLIENT SDK COMPONENTS +AVS DEVICE SDK COMPONENTS *************************** -The following Alexa Client SDK components are licensed under the Apache +The following AVS Device SDK components are licensed under the Apache License, Version 2.0 (the "License"): - Activity Focus Manager Library (AFML) @@ -13,6 +13,7 @@ License, Version 2.0 (the "License"): - Alexa Communications Library (ACL) - Alexa Directive Sequencer Library (ADSL) - Audio Input Processor (AIP) +- AudioPlayer Capability Agent - Key Word Detector (KWD) - Media Player - Sample App @@ -28,7 +29,16 @@ 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. +****************** +ALEXA AUDIO ASSETS +****************** +Copyright 2017 Amazon.com, Inc. or its affiliates (“Amazon”). +All Rights Reserved. + +These materials are licensed to you as "Alexa Materials" under the Alexa Voice +Service Agreement, which is currently available at +https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/support/terms-and-agreements. ********************** THIRD PARTY COMPONENTS diff --git a/PlaylistParser/CMakeLists.txt b/PlaylistParser/CMakeLists.txt new file mode 100644 index 0000000000..0950d4770b --- /dev/null +++ b/PlaylistParser/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.1 FATAL_ERROR) +project(PlaylistParser LANGUAGES CXX) + +include(../build/BuildDefaults.cmake) + +add_subdirectory("src") + +if(NOT TOTEM_PLPARSER) + message("Totem-Pl-Parser based playlist parser will not be built, and the playlist parser is disabled.") +else() + add_subdirectory("test") +endif() \ No newline at end of file diff --git a/PlaylistParser/include/PlaylistParser/DummyPlaylistParser.h b/PlaylistParser/include/PlaylistParser/DummyPlaylistParser.h new file mode 100644 index 0000000000..1f3a7b6602 --- /dev/null +++ b/PlaylistParser/include/PlaylistParser/DummyPlaylistParser.h @@ -0,0 +1,47 @@ +/* + * DummyPlaylistParser.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_PLAYLIST_PARSER_INCLUDE_PLAYLIST_PARSER_DUMMY_PLAYLIST_PARSER_H_ +#define ALEXA_CLIENT_SDK_PLAYLIST_PARSER_INCLUDE_PLAYLIST_PARSER_DUMMY_PLAYLIST_PARSER_H_ + +#include + +namespace alexaClientSDK { +namespace playlistParser { + +/** + * A simple dummy implementation of the PlaylistParser + * + */ +class DummyPlaylistParser : public avsCommon::utils::playlistParser::PlaylistParserInterface { +public: + /** + * Creates an instance of the @c DummyPlaylistParser. + * + * @return An instance of the @c DummyPlaylistParser if successful else a @c nullptr. + */ + static std::shared_ptr create(); + + bool parsePlaylist(const std::string& url, + std::shared_ptr observer) override; +}; + +} // namespace playlistParser +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_PLAYLIST_PARSER_INCLUDE_PLAYLIST_PARSER_DUMMY_PLAYLIST_PARSER_H_ + diff --git a/PlaylistParser/include/PlaylistParser/PlaylistParser.h b/PlaylistParser/include/PlaylistParser/PlaylistParser.h new file mode 100644 index 0000000000..e1085216c0 --- /dev/null +++ b/PlaylistParser/include/PlaylistParser/PlaylistParser.h @@ -0,0 +1,217 @@ +/* + * PlaylistParser.h + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 ALEXA_CLIENT_SDK_PLAYLIST_PARSER_INCLUDE_PLAYLIST_PARSER_PLAYLIST_PARSER_H_ +#define ALEXA_CLIENT_SDK_PLAYLIST_PARSER_INCLUDE_PLAYLIST_PARSER_PLAYLIST_PARSER_H_ + +#include +#include +#include +#include + +#include + +#include + +namespace alexaClientSDK { +namespace playlistParser { + +/** + * This implementation of the PlaylistParser uses the Totem Playlist Parser library. + * @see https://github.com/GNOME/totem-pl-parser + * @see https://developer.gnome.org/totem-pl-parser/stable/TotemPlParser.html. + * + * @note For the playlist parser to function properly, a main event loop needs to be running. If there is no main event + * loop the signals related to parsing playlist will not be called. + */ +class PlaylistParser : public avsCommon::utils::playlistParser::PlaylistParserInterface { +public: + /** + * Creates an instance of the @c PlaylistParser. + * + * @return An instance of the @c PlaylistParser if successful else a @c nullptr. + */ + static std::unique_ptr create(); + + /** + * Destructor. + */ + ~PlaylistParser(); + + bool parsePlaylist(const std::string& url, + std::shared_ptr observer) override; + +private: + /** + * Class which has all the information to parse a playlist and the observer to whom the notification needs to be + * sent once the parsing has been completed. + */ + class PlaylistInfo { + public: + /// Constructor. It initializes the parser. + PlaylistInfo(TotemPlParser* parser); + + /// Destructor. + ~PlaylistInfo(); + + /// An instance of the @c TotemPlParser. + TotemPlParser* parser; + + /// The playlist url. + std::string playlistUrl; + + /// An instance of the observer to notify once parsing is complete. + std::shared_ptr observer; + + /// A queue of urls extracted from the playlist. + std::queue urlQueue; + + /// ID of the handler installed to receive playlist started data signals. + guint playlistStartedHandlerId; + + /// ID of the handler installed to receive entry parsed data signals. + guint entryParsedHandlerId; + + /// Mutex to serialize access to @c urlQueue. + std::mutex mutex; + }; + + /** + * Initializes the @c m_parserThread. + */ + PlaylistParser(); + + /** + * Creates an instance of the @c PlaylistInfo and initializes its parser with an instance of the @c TotemPlParser. + * Connects signals for the parser to the callbacks. + * + * @param url The url of the playlist to be parsed. + * @param observer The observer to be notified of when playlist parsing is complete. + * @return A @c PlaylistInfo if successful else @c nullptr. + * + * @note This should be called only after validating the url and observer. + */ + std::shared_ptr createPlaylistInfo( + const std::string& url, + std::shared_ptr observer); + + /** + * Callback for when playlist parsing begins. + * + * @param parser The instance of the @c TotemPlParser. + * @param url The playlist url being parsed. + * @param metadata The metadata associated with the playlist. + * @param playlistInfo The instance of the @c PlaylistInfo associated with the parser. + */ + static void onPlaylistStarted( + TotemPlParser* parser, + gchar* url, + TotemPlParserMetadata* metadata, + gpointer playlistInfo); + + /** + * Callback for when an entry has been extracted from the playlist. + * + * @param parser The instance of the @c TotemPlParser. + * @param url The url entry extracted from the playlist. + * @param metadata The metadata associated with the entry. + * @param playlistInfo The instance of the @c PlaylistInfo associated with the parser. + */ + static void onEntryParsed( + TotemPlParser* parser, + gchar* url, + TotemPlParserMetadata* metadata, + gpointer playlistInfo); + + /** + * Callback for when the parsing is complete. + * + * @param parser The instance of the @c TotemPlParser. + * @param result The result of the playlist parsing. + * @param playlistInfo The instance of the @c PlaylistInfo associated with the parser. + */ + static void onParseComplete(GObject* parser, GAsyncResult* result, gpointer playlistInfo); + + /** + * Calls the function to start playlist parsing. This function is called after acquiring a lock to @c m_mutex. + * The function will release the lock before calling the Totem parsing function. + * + * @param lock The lock acquired on @c m_mutex. + */ + void handleParsingLocked(std::unique_lock& lock); + + /** + * Adds the url entry to the queue of entries to be sent to the observer. + * + * @param url The url entry extracted from the playlist. + * @param playlistInfo The instance of the @c PlaylistInfo associated with the parser. + */ + void handleOnEntryParsed(gchar *url, std::shared_ptr playlistInfo); + + /** + * Translates the @c result to @c PlaylistParseResult and calls @c onPlaylistParsed. + * + * @param result The result of parsing the playlist. + * @param playlistInfo The instance of the @c PlaylistInfo associated with the parser. + */ + void handleOnParseComplete(GAsyncResult* result, std::shared_ptr playlistInfo); + + /** + * Translates the @c TotemPlParserResult to a @c PlaylistParseResult. + * + * @param result The @c TotemPlParserResult of playlist parsing. + * @return the @c PlaylistParseResult + */ + avsCommon::utils::playlistParser::PlaylistParseResult mapResult(TotemPlParserResult result); + + /** + * Method that processes the playlist parsing requests in the @c m_playlistInfoQueue + */ + void parsingLoop(); + + /// The @c PlaylistInfo currently being processed. + std::shared_ptr m_playlistInfo; + + /// The thread on which the playlist parse requests are executed. + std::thread m_parserThread; + + /** + * Flag to indicate if a playlist is currently being parsed. @c m_mutex must be acquired before accessing this flag. + */ + bool m_isParsingActive; + + /** + * Flag to indicate whether the playlist parser is shutting down. @c m_mutex must be acquired before accessing this + * flag. + */ + bool m_isShuttingDown; + + /// Condition variable used to wake @c parsingLoop. + std::condition_variable m_wakeParsingLoop; + + /// Queue to which the playlist parsing requests are added. @c m_mutex must be acquired before accessing this queue. + std::deque> m_playlistInfoQueue; + + /// Mutex which serializes access to @c m_isParsingActive, @c m_isShuttingDown, @c m_playlistInfoQueue. + std::mutex m_mutex; +}; + +} // namespace playlistParser +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_PLAYLIST_PARSER_INCLUDE_PLAYLIST_PARSER_PLAYLIST_PARSER_H_ + diff --git a/PlaylistParser/inputs/sample.m3u8 b/PlaylistParser/inputs/sample.m3u8 new file mode 100644 index 0000000000..2f159ece91 --- /dev/null +++ b/PlaylistParser/inputs/sample.m3u8 @@ -0,0 +1 @@ +#EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:9684358, #EXTINF:10,RADIO http://76.74.255.139/bismarck/live/bismarck.mov_9684358.aac #EXTINF:10,RADIO http://76.74.255.139/bismarck/live/bismarck.mov_9684359.aac #EXTINF:10,RADIO http://76.74.255.139/bismarck/live/bismarck.mov_9684360.aac diff --git a/PlaylistParser/inputs/sample1.asx b/PlaylistParser/inputs/sample1.asx new file mode 100644 index 0000000000..a1d092d709 --- /dev/null +++ b/PlaylistParser/inputs/sample1.asx @@ -0,0 +1,10 @@ + + + Item 1 + + + + Item 2 + + + diff --git a/PlaylistParser/inputs/sample2.m3u b/PlaylistParser/inputs/sample2.m3u new file mode 100644 index 0000000000..4e92fe558e --- /dev/null +++ b/PlaylistParser/inputs/sample2.m3u @@ -0,0 +1,2 @@ +http://stream.radiotime.com/sample.mp3 +http://live-mp3-128.kexp.org diff --git a/PlaylistParser/inputs/sample3.pls b/PlaylistParser/inputs/sample3.pls new file mode 100644 index 0000000000..016a5632f1 --- /dev/null +++ b/PlaylistParser/inputs/sample3.pls @@ -0,0 +1,9 @@ +[playlist] +NumberOfEntries=2 + +File1=http://stream.radiotime.com/sample.mp3 +Length1=-1 + +File2=http://live-mp3-128.kexp.org +Length2=-1 + diff --git a/PlaylistParser/src/CMakeLists.txt b/PlaylistParser/src/CMakeLists.txt new file mode 100644 index 0000000000..789fc8d013 --- /dev/null +++ b/PlaylistParser/src/CMakeLists.txt @@ -0,0 +1,18 @@ +add_definitions("-DACSDK_LOG_MODULE=PlaylistParser") + +if(TOTEM_PLPARSER) + add_library(PlaylistParser SHARED PlaylistParser.cpp) + + target_include_directories(PlaylistParser PUBLIC + "${PlaylistParser_SOURCE_DIR}/include" + ${TOTEM_INCLUDE_DIRS}) + + target_link_libraries(PlaylistParser ${TOTEM_LDFLAGS} AVSCommon) +else() + add_library(PlaylistParser SHARED DummyPlaylistParser.cpp) + + target_include_directories(PlaylistParser PUBLIC + "${PlaylistParser_SOURCE_DIR}/include") + + target_link_libraries(PlaylistParser AVSCommon) +endif() diff --git a/PlaylistParser/src/DummyPlaylistParser.cpp b/PlaylistParser/src/DummyPlaylistParser.cpp new file mode 100644 index 0000000000..8f14e94c25 --- /dev/null +++ b/PlaylistParser/src/DummyPlaylistParser.cpp @@ -0,0 +1,67 @@ +/* + * DummyPlaylistParser.cpp + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 "PlaylistParser/DummyPlaylistParser.h" + +namespace alexaClientSDK { +namespace playlistParser { + +/// String to identify log entries originating from this file. +static const std::string TAG("DummyPlaylistParser"); + +/** + * Create a LogEntry using this file's TAG and the specified event string. + * + * @param The event string for this @c LogEntry. + */ +#define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) + +std::shared_ptr DummyPlaylistParser::create() { + ACSDK_DEBUG9(LX("createCalled")); + auto playlistParser = std::make_shared(); + return playlistParser; +} + +bool DummyPlaylistParser::parsePlaylist(const std::string& url, + std::shared_ptr observer) { + ACSDK_DEBUG9(LX("parsePlaylist").d("url", url)); + + if (url.empty()) { + ACSDK_ERROR(LX("parsePlaylistFailed").d("reason","emptyUrl")); + return false; + } + + if (!observer) { + ACSDK_ERROR(LX("parsePlaylistFailed").d("reason","observerIsNullptr")); + return false; + } + + // An empty queue to be passed to the observer + std::queue emptyUrlQueue; + + observer->onPlaylistParsed( + url, + emptyUrlQueue, + avsCommon::utils::playlistParser::PlaylistParseResult::PARSE_RESULT_UNHANDLED); + + return true; +} + +} // namespace playlistParser +} // namespace alexaClientSDK diff --git a/PlaylistParser/src/PlaylistParser.cpp b/PlaylistParser/src/PlaylistParser.cpp new file mode 100644 index 0000000000..c90fbc3abf --- /dev/null +++ b/PlaylistParser/src/PlaylistParser.cpp @@ -0,0 +1,246 @@ +/* + * PlaylistParser.cpp + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 "PlaylistParser/PlaylistParser.h" + +namespace alexaClientSDK { +namespace playlistParser { + +/// String to identify log entries originating from this file. +static const std::string TAG("PlaylistParser"); + +/** + * Create a LogEntry using this file's TAG and the specified event string. + * + * @param The event string for this @c LogEntry. + */ +#define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) + +std::unique_ptr PlaylistParser::create() { + ACSDK_DEBUG9(LX("createCalled")); + std::unique_ptr playlistParser(new PlaylistParser()); + return playlistParser; +} + +PlaylistParser::~PlaylistParser() { + ACSDK_DEBUG9(LX("destructorCalled")); + { + std::unique_lock lock(m_mutex); + m_isShuttingDown = true; + for (auto info : m_playlistInfoQueue) { + g_signal_handler_disconnect(info->parser, info->playlistStartedHandlerId); + g_signal_handler_disconnect(info->parser, info->entryParsedHandlerId); + info->observer->onPlaylistParsed( + info->playlistUrl, + info->urlQueue, + avsCommon::utils::playlistParser::PlaylistParseResult::PARSE_RESULT_CANCELLED); + } + m_playlistInfoQueue.clear(); + m_playlistInfo = nullptr; + } + m_wakeParsingLoop.notify_one(); + if (m_parserThread.joinable()) { + m_parserThread.join(); + } +} + +bool PlaylistParser::parsePlaylist(const std::string& url, + std::shared_ptr observer) { + ACSDK_DEBUG9(LX("parsePlaylist").d("url", url)); + + if (url.empty()) { + ACSDK_ERROR(LX("parsePlaylistFailed").d("reason","emptyUrl")); + return false; + } + + if (!observer) { + ACSDK_ERROR(LX("parsePlaylistFailed").d("reason","observerIsNullptr")); + return false; + } + + auto playlistInfo = createPlaylistInfo(url, observer); + if (!playlistInfo) { + ACSDK_ERROR(LX("parsePlaylistFailed").d("reason", "cannotCreateNewPlaylistInfo")); + return false; + } + + playlistInfo->playlistUrl = url; + playlistInfo->observer = observer; + + { + std::lock_guard lock(m_mutex); + m_playlistInfoQueue.push_back(playlistInfo); + m_wakeParsingLoop.notify_one(); + } + return true; +} + +PlaylistParser::PlaylistInfo::PlaylistInfo(TotemPlParser* plParser) + : + parser{plParser}, + playlistStartedHandlerId{0}, + entryParsedHandlerId{0} { +} + + +PlaylistParser::PlaylistInfo::~PlaylistInfo() { + g_clear_object(&parser); +} + +PlaylistParser::PlaylistParser() + : + m_isParsingActive{false}, + m_isShuttingDown{false} { + m_parserThread = std::thread(&PlaylistParser::parsingLoop, this); +} + +std::shared_ptr PlaylistParser::createPlaylistInfo( + const std::string& url, + std::shared_ptr observer) { + ACSDK_DEBUG9(LX("createPlaylistInfo")); + + TotemPlParser* parser = totem_pl_parser_new(); + if (!parser) { + ACSDK_ERROR(LX("createPlaylistInfoFailed").d("reason", "cannotCreateNewParser")); + return nullptr; + } + + std::shared_ptr playlistInfo = std::make_shared(parser); + + g_object_set(parser, "recurse", TRUE, "disable-unsafe", TRUE, "force", TRUE, NULL); + + playlistInfo->playlistStartedHandlerId = g_signal_connect(G_OBJECT(parser), "playlist-started", + G_CALLBACK(onPlaylistStarted), this); + if (!playlistInfo->playlistStartedHandlerId) { + ACSDK_ERROR(LX("createPlaylistInfoFailed").d("reason", "cannotConnectPlaylistStartedSignal")); + return nullptr; + } + + playlistInfo->entryParsedHandlerId = g_signal_connect(G_OBJECT(parser), "entry-parsed", + G_CALLBACK(onEntryParsed), this); + if (!playlistInfo->entryParsedHandlerId) { + ACSDK_ERROR(LX("createPlaylistInfoFailed").d("reason", "cannotConnectEntryParsedSignal")); + g_signal_handler_disconnect(playlistInfo->parser, playlistInfo->playlistStartedHandlerId); + return nullptr; + } + + return playlistInfo; +} + +void PlaylistParser::onPlaylistStarted (TotemPlParser *parser, gchar *url, TotemPlParserMetadata *metadata, + gpointer pointer) { + ACSDK_DEBUG9(LX("onPlaylistStarted").d("url", url)); +} + +void PlaylistParser::onEntryParsed(TotemPlParser *parser, gchar *url, TotemPlParserMetadata *metadata, + gpointer pointer) { + ACSDK_DEBUG9(LX("onEntryParsed").d("url", url)); + auto playlistParser = static_cast(pointer); + if (playlistParser && playlistParser->m_playlistInfo){ + playlistParser->handleOnEntryParsed(url, playlistParser->m_playlistInfo); + } +} + +void PlaylistParser::onParseComplete(GObject* parser, GAsyncResult* result, gpointer pointer) { + ACSDK_DEBUG9(LX("onParseComplete")); + auto playlistParser = static_cast(pointer); + if (playlistParser && playlistParser->m_playlistInfo) { + playlistParser->handleOnParseComplete(result, playlistParser->m_playlistInfo); + } +} + +void PlaylistParser::handleParsingLocked(std::unique_lock& lock) { + ACSDK_DEBUG9(LX("handleParsingLocked")); + m_playlistInfo = m_playlistInfoQueue.front(); + m_isParsingActive = true; + lock.unlock(); + totem_pl_parser_parse_async(m_playlistInfo->parser, m_playlistInfo->playlistUrl.c_str(), FALSE, NULL, + onParseComplete, this); +} + +void PlaylistParser::handleOnEntryParsed(gchar *url, std::shared_ptr playlistInfo) { + ACSDK_DEBUG9(LX("handleOnEntryParsed").d("url", url)); + std::lock_guard lock(playlistInfo->mutex); + playlistInfo->urlQueue.push(url); +} + +void PlaylistParser::handleOnParseComplete(GAsyncResult* result, std::shared_ptr playlistInfo) { + ACSDK_DEBUG9(LX("handleOnParseComplete")); + GError *error = nullptr; + auto parserResult = totem_pl_parser_parse_finish (playlistInfo->parser, result, &error); + std::queue urlQueue; + + { + std::lock_guard lock(playlistInfo->mutex); + urlQueue = playlistInfo->urlQueue; + } + + playlistInfo->observer->onPlaylistParsed( + playlistInfo->playlistUrl, + urlQueue, + mapResult(parserResult)); + + { + std::lock_guard lock(m_mutex); + if (!m_playlistInfoQueue.empty()) { + m_playlistInfoQueue.pop_front(); + } + m_isParsingActive = false; + m_wakeParsingLoop.notify_one(); + } +} + +avsCommon::utils::playlistParser::PlaylistParseResult PlaylistParser::mapResult(TotemPlParserResult result) { + switch(result) { + case TOTEM_PL_PARSER_RESULT_SUCCESS: + ACSDK_DEBUG9(LX("playlistParsingSuccessful")); + return avsCommon::utils::playlistParser::PlaylistParseResult::PARSE_RESULT_SUCCESS; + case TOTEM_PL_PARSER_RESULT_UNHANDLED: + ACSDK_DEBUG9(LX("playlistCouldNotBeHandled")); + return avsCommon::utils::playlistParser::PlaylistParseResult::PARSE_RESULT_UNHANDLED; + case TOTEM_PL_PARSER_RESULT_ERROR: + ACSDK_DEBUG9(LX("playlistParsingError")); + return avsCommon::utils::playlistParser::PlaylistParseResult::PARSE_RESULT_ERROR; + case TOTEM_PL_PARSER_RESULT_IGNORED: + ACSDK_DEBUG9(LX("playlistWasIgnoredDueToSchemeOrMimeType")); + return avsCommon::utils::playlistParser::PlaylistParseResult::PARSE_RESULT_IGNORED; + case TOTEM_PL_PARSER_RESULT_CANCELLED: + ACSDK_DEBUG9(LX("playlistParsingWasCancelledPartWayThrough")); + return avsCommon::utils::playlistParser::PlaylistParseResult::PARSE_RESULT_CANCELLED; + } + return avsCommon::utils::playlistParser::PlaylistParseResult::PARSE_RESULT_ERROR; +} + +void PlaylistParser::parsingLoop() { + auto wake = [this]() { + return (m_isShuttingDown || (!m_playlistInfoQueue.empty() && !m_isParsingActive)); + }; + + while (true) { + std::unique_lock lock(m_mutex); + m_wakeParsingLoop.wait(lock, wake); + if (m_isShuttingDown) { + break; + } + handleParsingLocked(lock); + } +} + +} // namespace playlistParser +} // namespace alexaClientSDK diff --git a/PlaylistParser/test/CMakeLists.txt b/PlaylistParser/test/CMakeLists.txt new file mode 100644 index 0000000000..5e0d863561 --- /dev/null +++ b/PlaylistParser/test/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.0) + +find_package(Threads ${THREADS_PACKAGE_CONFIG}) + +add_definitions("-DACSDK_LOG_MODULE=playlistParserTest") + +set(INCLUDES "${PlaylistParser_SOURCE_DIR}/include") + +set(LIBRARIES PlaylistParser ${CMAKE_THREAD_LIBS_INIT}) + +set(INPUT_FOLDER "${PlaylistParser_SOURCE_DIR}/inputs") + +discover_unit_tests("${INCLUDES}" "${LIBRARIES}" "${INPUT_FOLDER}") diff --git a/PlaylistParser/test/PlaylistParserTest.cpp b/PlaylistParser/test/PlaylistParserTest.cpp new file mode 100644 index 0000000000..c2be0e9fe7 --- /dev/null +++ b/PlaylistParser/test/PlaylistParserTest.cpp @@ -0,0 +1,251 @@ +/* + * PlaylistParserTest.cpp + * + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 "PlaylistParser/PlaylistParser.h" + +namespace alexaClientSDK { +namespace playlistParser { +namespace test { + +using namespace avsCommon::utils::playlistParser; +using namespace ::testing; + +/// String to identify log entries originating from this file. +static const std::string TAG("PlaylistParserTest"); + +/** + * Create a LogEntry using this file's TAG and the specified event string. + * + * @param The event string for this @c LogEntry. + */ +#define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) + +/// The path to the input Dir containing the test audio files. +std::string inputsDirPath; + +/// The prefix for the file URI scheme. +static const std::string FILE_URI_PREFIX("file://"); + +/// Test playlist url. +static const std::string TEST_PLAYLIST{"/sample2.m3u"}; + +/// A test playlist url. One of the links on this playlist redirects to another playlist. +static const std::string TEST_ASX_PLAYLIST{"/sample1.asx"}; + +/// A test playlist in PLS format. +static const std::string TEST_PLS_PLAYLIST{"/sample3.pls"}; + +/// A test playlist in HLS format. +static const std::string TEST_HLS_PLAYLIST{"/sample.m3u8"}; + +/** + * Mock AttachmentReader. + */ +class TestParserObserver : public avsCommon::utils::playlistParser::PlaylistParserObserverInterface { +public: + /** + * Creates an instance of the @c TestParserObserver. + * @return An instance of the @c TestParserObserver. + */ + static std::shared_ptr create(); + + /// Destructor + ~TestParserObserver() = default; + + void onPlaylistParsed(std::string playlistUrl, std::queue urls, + avsCommon::utils::playlistParser::PlaylistParseResult parseResult) override; + + /** + * Waits for playlist parsing to complete. + * + * @param expectedResult The expected result from parsing the playlist. + * @param duration The duration to wait for playlist parsing to complete. + * @return The result of parsing the playlist. + */ + bool waitForPlaylistParsed(const PlaylistParseResult expectedResult, + const std::chrono::seconds duration = std::chrono::seconds(5)); + + /** + * Constructor. + */ + TestParserObserver(); + + /// Mutex to serialize access to @c m_playlistParsed and @c m_parseResult. + std::mutex m_mutex; + + /// Condition Variable to wake @c waitForPlaylistParsed + std::condition_variable m_wakePlaylistParsed; + + /// Flag to indicate when playlist has been parsed. + bool m_playlistParsed; + + /// The result of parsing the playlist. + PlaylistParseResult m_parseResult; + + /// The initial playlist Url. + std::string m_initialUrl; + + /// Urls extracted from the playlist. + std::queue m_urls; +}; + +std::shared_ptr TestParserObserver::create() { + std::shared_ptr playlistObserver(new TestParserObserver); + return playlistObserver; +} + +void TestParserObserver::onPlaylistParsed(std::string playlistUrl, std::queue urls, + avsCommon::utils::playlistParser::PlaylistParseResult parseResult) { + std::lock_guard lock(m_mutex); + m_playlistParsed = true; + m_parseResult = parseResult; + m_urls = urls; + m_initialUrl = playlistUrl; + m_wakePlaylistParsed.notify_one(); +} + +bool TestParserObserver::waitForPlaylistParsed(const PlaylistParseResult expectedResult, + const std::chrono::seconds duration) { + std::unique_lock lock(m_mutex); + if (!m_wakePlaylistParsed.wait_for(lock, duration, [this]() { return m_playlistParsed; } )) + { + return false; + } + return (expectedResult == m_parseResult); +} + +TestParserObserver::TestParserObserver() + : + m_playlistParsed{false}, + m_parseResult{PlaylistParseResult::PARSE_RESULT_ERROR} { +} + +class PlaylistParserTest: public ::testing::Test{ +public: + void SetUp() override; + + void TearDown() override; + + /// Instance of the @c PlaylistParser. + std::shared_ptr m_playlistParser; + + /// Instance of the @c TestParserObserver. + std::shared_ptr m_parserObserver; + + /// The main event loop. + GMainLoop* m_loop; + + /// The thread on which the main event loop is launched. + std::thread m_mainLoopThread; + +}; + +void PlaylistParserTest::SetUp() { + m_loop = g_main_loop_new(nullptr, false); + ASSERT_TRUE(m_loop); + m_mainLoopThread = std::thread(g_main_loop_run, m_loop); + m_parserObserver = TestParserObserver::create(); + m_playlistParser = PlaylistParser::create(); + ASSERT_TRUE(m_playlistParser); +} + +void PlaylistParserTest::TearDown() { + while (!g_main_loop_is_running(m_loop)){ + std::this_thread::yield(); + } + g_main_loop_quit(m_loop); + if (m_mainLoopThread.joinable()) { + m_mainLoopThread.join(); + } + g_main_loop_unref(m_loop); +} + +/** + * Tests parsing of an empty playlist. Calls @c parsePlaylist and expects it returns false. + */ +TEST_F(PlaylistParserTest, testEmptyUrl) { + ASSERT_FALSE(m_playlistParser->parsePlaylist("", m_parserObserver)); +} + +/** + * Tests passing a @c nullptr for the observer. + */ +TEST_F(PlaylistParserTest, testNullObserver) { + ASSERT_FALSE(m_playlistParser->parsePlaylist(FILE_URI_PREFIX + inputsDirPath + TEST_PLAYLIST, nullptr)); +} + +/** + * Tests parsing of a single playlist. + * Calls @c parsePlaylist and expects that the result of the parsing is successful. + */ +TEST_F(PlaylistParserTest, testParsingPlaylist) { + ASSERT_TRUE(m_playlistParser->parsePlaylist(FILE_URI_PREFIX + inputsDirPath + TEST_PLAYLIST, m_parserObserver)); + ASSERT_TRUE(m_parserObserver->waitForPlaylistParsed(PlaylistParseResult::PARSE_RESULT_SUCCESS)); +} + +/** + * Tests parsing of a PLS playlist. + * Calls @c parsePlaylist and expects that the result of the parsing is successful. + */ +TEST_F(PlaylistParserTest, testParsingPlsPlaylist) { + ASSERT_TRUE(m_playlistParser->parsePlaylist(FILE_URI_PREFIX + inputsDirPath + TEST_PLS_PLAYLIST, m_parserObserver)); + ASSERT_TRUE(m_parserObserver->waitForPlaylistParsed(PlaylistParseResult::PARSE_RESULT_SUCCESS)); +} + +/** + * Tests parsing of multiple playlists one of which is a recursive playlist. + * Calls @c parsePlaylist on the different playlists and expects each of them to be parsed successfully. + */ +TEST_F(PlaylistParserTest, testParsingMultiplePlaylists) { + auto m_parserObserver2 = TestParserObserver::create(); + ASSERT_TRUE(m_playlistParser->parsePlaylist(FILE_URI_PREFIX + inputsDirPath + TEST_PLAYLIST, m_parserObserver)); + ASSERT_TRUE(m_playlistParser->parsePlaylist(FILE_URI_PREFIX + inputsDirPath + TEST_ASX_PLAYLIST, m_parserObserver2)); + ASSERT_TRUE(m_parserObserver->waitForPlaylistParsed((PlaylistParseResult::PARSE_RESULT_SUCCESS))); + ASSERT_TRUE(m_parserObserver2->waitForPlaylistParsed((PlaylistParseResult::PARSE_RESULT_SUCCESS))); +} + +/** + * Tests parsing of a single playlist which is a format that cannot be handled. + * Calls @c parsePlaylist and expects that the result of the parsing is an error. + */ +TEST_F(PlaylistParserTest, testUnparsablePlaylist) { + ASSERT_TRUE(m_playlistParser->parsePlaylist(FILE_URI_PREFIX + inputsDirPath + TEST_HLS_PLAYLIST, m_parserObserver)); + ASSERT_FALSE(m_parserObserver->waitForPlaylistParsed((PlaylistParseResult::PARSE_RESULT_SUCCESS))); +} + +} // namespace test +} // namespace playlistParser +} // namespace alexaClientSDK + +int main (int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + + if (argc < 2) { + std::cerr << "Usage: PlaylistParserTest " << std::endl; + } else { + alexaClientSDK::playlistParser::test::inputsDirPath = std::string(argv[1]); + return RUN_ALL_TESTS(); + } +} diff --git a/README.md b/README.md index 93786c910c..4efc16794a 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,14 @@ -## Alexa Client SDK v0.6 +## AVS Device SDK v1.0.0 -This release introduces an implementation of the `Alerts` capability agent with support for timers and alarms, as well as classes used to handle directives and events in the [Systems interface](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/system), and a sample app that demonstrates SDK functionality. - -**NOTE**: This release requires a filesystem to store audio assets for timers and alarms. - -Native components for the following capability agents are **not** included in this release and will be added in future releases: `AudioPlayer`, `PlaybackController`, `Speaker`, and `Settings`. However, it's important to note, this release does support directives from the referenced interfaces. +Please note the following for this release: +* Native components for the following capability agents are **not** included in v1.0 and will be added in future releases: `PlaybackController`, `Speaker`, and `Settings`. +* iHeartRadio is supported. Support for additional audio codecs, containers, streaming formats, and playlists will be included in future releases. ## Overview -The Alexa Client SDK provides a modern C++ (11 or later) interface for the Alexa Voice Service (AVS) that allows developers to add intelligent voice control to connected products. It is modular and abstracted, providing components to handle discrete functionality such as speech capture, audio processing, and communications, with each component exposing APIs that you can use and customize for your integration. +The AVS Device SDK for C++ provides a modern C++ (11 or later) interface for the Alexa Voice Service (AVS) that allows developers to add intelligent voice control to connected products. It is modular and abstracted, providing components to handle discrete functionality such as speech capture, audio processing, and communications, with each component exposing APIs that you can use and customize for your integration. It also includes a sample app, which demonstrates interactions with the Alexa Voice Service (AVS). -The SDK also includes a sample app that demonstrates interactions with AVS. +To quickly setup your Raspberry Pi development environment or to learn how to optimize libcurl for size, [click here](https://github.com/alexa/alexa-client-sdk/wiki) to see the wiki. * [Common Terms](#common-terms) * [SDK Components](#sdk-components) @@ -21,7 +19,7 @@ The SDK also includes a sample app that demonstrates interactions with AVS. * [Run Unit Tests](#run-unit-tests) * [Run Integration Tests](#run-integration-tests) * [Run the Sample App](#run-the-sample-app) -* [Alexa Client SDK API Documentation(Doxygen)](#alexa-client-sdk-api-documentation) +* [AVS Device SDK for C++ API Documentation(Doxygen)](#alexa-client-sdk-api-documentation) * [Resources and Guides](#resources-and-guides) * [Release Notes](#release-notes) @@ -35,9 +33,9 @@ The SDK also includes a sample app that demonstrates interactions with AVS. ## SDK Components -This architecture diagram illustrates the data flows between components that comprise the Alexa Client SDK. +This architecture diagram illustrates the data flows between components that comprise the AVS Device SDK for C++. -![SDK Architecture Diagram](https://images-na.ssl-images-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-voice-service/docs/avs-cpp-sdk-architecture-20170601.png) +![SDK Architecture Diagram](https://m.media-amazon.com/images/G/01/mobile-apps/dex/avs/Alexa_Device_SDK_Architecture.png) **Audio Signal Processor (ASP)** - Applies signal processing algorithms to both input and output audio channels. The applied algorithms are designed to produce clean audio data and include, but are not limited to: acoustic echo cancellation (AEC), beam forming (fixed or adaptive), voice activity detection (VAD), and dynamic range compression (DRC). If a multi-microphone array is present, the ASP constructs and outputs a single audio stream for the array. @@ -76,7 +74,9 @@ Focus management is not specific to Capability Agents or Directive Handlers, and * [Speaker](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/speaker) - The interface for volume control, including mute and unmute. * [System](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/system) - The interface for communicating product status/state to AVS. -## Minimum Requirements and Dependencies +## Minimum Requirements and Dependencies + +Instructions are available to help you quickly [setup a development environment for RaspberryPi](#resources-and-guides), and [build libcurl with nghttp2 for macOS](#resources-and-guides). * C++ 11 or later * [GNU Compiler Collection (GCC) 4.8.5](https://gcc.gnu.org/) or later **OR** [Clang 3.3](http://clang.llvm.org/get_started.html) or later @@ -85,8 +85,10 @@ Focus management is not specific to Capability Agents or Directive Handlers, and * [nghttp2 1.0](https://github.com/nghttp2/nghttp2) or later * [OpenSSL 1.0.2](https://www.openssl.org/source/) or later * [Doxygen 1.8.13](http://www.stack.nl/~dimitri/doxygen/download.html) or later (required to build API documentation) -* **NEW** - [SQLite 3.19.3](https://www.sqlite.org/download.html) or later (required for Alerts) -* **NEW** - For Alerts to work as expected, the device system clock must be set to UTC time. We recommend using `NTP` to do this. +* [SQLite 3.19.3](https://www.sqlite.org/download.html) or later (required for Alerts) +* For Alerts to work as expected: + * The device system clock must be set to UTC time. We recommend using `NTP` to do this + * A filesystem is required **MediaPlayer Reference Implementation Dependencies** Building the reference implementation of the `MediaPlayerInterface` (the class `MediaPlayer`) is optional, but requires: @@ -97,14 +99,14 @@ Building the reference implementation of the `MediaPlayerInterface` (the class ` * [GStreamer Libav Plugin 1.8](https://gstreamer.freedesktop.org/releases/gst-libav/1.8.0.html) or later **OR** [GStreamer Ugly Plugins 1.8](https://gstreamer.freedesktop.org/releases/gst-plugins-ugly/1.8.0.html) or later, for decoding MP3 data. -**NOTE**: The plugins may depend on libraries which need to be installed as well for the GStreamer based `MediaPlayer` to work correctly. +**NOTE**: The plugins may depend on libraries which need to be installed for the GStreamer based `MediaPlayer` to work correctly. **Sample App Dependencies** Building the sample app is optional, but requires: * [PortAudio v190600_20161030](http://www.portaudio.com/download.html) * GStreamer -**NOTE**: The sample app will still work with or without the SDK being built with a wake word engine. If built without a wake word engine, hands-free mode will be disabled in the sample app. +**NOTE**: The sample app will work with or without a wake word engine. If built without a wake word engine, hands-free mode will be disabled in the sample app. ## Prerequisites @@ -156,12 +158,14 @@ The following build types are supported: * `RELEASE` - Adds `-O2` flag and removes `-g` flag. * `MINSIZEREL` - Compiles with `RELEASE` flags and optimizations (`-O`s) for a smaller build size. -To specify a build type, use this command in place of step 4 below (see [Build for Generic Linux](#generic-linux) or [Build for macOS](#build-for-macosß)): -`cmake -DCMAKE_BUILD_TYPE=` +To specify a build type, use this command in place of step 4 below: +`cmake -DCMAKE_BUILD_TYPE=` -### Build with a Wake Word Detector +### Build with a Wake Word Detector -The Alexa Client SDK supports wake word detectors from [Sensory](https://github.com/Sensory/alexa-rpi) and [KITT.ai](https://github.com/Kitt-AI/snowboy/). The following options are required to build with a wake word detector, please replace `` with `SENSORY` for Sensory, and `KITTAI` for KITT.ai: +**Note**: Wake word detector and key word detector (KWD) are used interchangeably. + +The AVS Device SDK for C++ supports wake word detectors from [Sensory](https://github.com/Sensory/alexa-rpi) and [KITT.ai](https://github.com/Kitt-AI/snowboy/). The following options are required to build with a wake word detector, please replace `` with `SENSORY` for Sensory, and `KITTAI` for KITT.ai: * `-D_KEY_WORD_DETECTOR=` - Specifies if the wake word detector is enabled or disabled during build. * `-D_KEY_WORD_DETECTOR_LIB_PATH=` - The path to the wake word detector library. @@ -197,7 +201,9 @@ cmake -DKITTAI_KEY_WORD_DETECTOR=ON -DKITTAI_KEY_WORD_DETECTOR_ `MediaPlayer` (the reference implementation of the `MediaPlayerInterface`) is based upon [GStreamer](https://gstreamer.freedesktop.org/), and is not built by default. To build 'MediaPlayer' the `-DGSTREAMER_MEDIA_PLAYER=ON` option must be specified to CMake. -If GStreamer was [installed from source](https://gstreamer.freedesktop.org/documentation/frequently-asked-questions/getting.html), the prefix path provided when building must be specified to CMake with the `DCMAKE_PREFIX_PATH` option. This is an example CMake command: +If GStreamer was [installed from source](https://gstreamer.freedesktop.org/documentation/frequently-asked-questions/getting.html), the prefix path provided when building must be specified to CMake with the `DCMAKE_PREFIX_PATH` option. + +This is an example `cmake` command: ``` cmake -DGSTREAMER_MEDIA_PLAYER=ON -DCMAKE_PREFIX_PATH= @@ -205,9 +211,9 @@ cmake -DGSTREAMER_MEDIA_PLAYER=ON -DCMAKE_PREFIX_PATH= -DPORTAUDIO=ON @@ -221,65 +227,44 @@ cmake -DPORTAUDIO=ON -DPORTAUDIO_LIB_PATH=.../portaudio/lib/.libs/libportaudio.a -DPORTAUDIO_INCLUDE_DIR=.../portaudio/include ``` -### Application Settings - -The SDK will require a configuration JSON file, an example of which is located in `Integration/AlexaClientSDKConfig.json`. The contents of the JSON should be populated with your product information (which you got from the developer portal when registering a product and creating a security profile), and the location of your database and sound files. This JSON file is required for the integration tests to work properly, as well as for the Sample App. - -The layout of the file is as follows: -```json -{ -"authDelegate":{ -"deviceTypeId":"", -"clientId":"", -"clientSecret":"", -"deviceSerialNumber":"" -}, -"alertsCapabilityAgent": { -"databaseFilePath":"//", -"alarmSoundFilePath":"//alarm_normal.mp3", -"alarmShortSoundFilePath":"//alarm_short.wav", -"timerSoundFilePath":"//timer_normal.mp3", -"timerShortSoundFilePath":"//timer_short.wav" -} -} -``` -**NOTE**: The `deviceSerialNumber` is a unique identifier that you create. It is **not** provided by Amazon. -**NOTE**: The audio files for the alerts can be downloaded from [here](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/content/alexa-voice-service-ux-design-guidelines#attention). Note that the Alexa Voice Service UX guidelines mandate that these audio files must be used for Alexa alerts. -### Build for Generic Linux +## Build for Generic Linux / Raspberry Pi / macOS -To create an out-of-source build for Linux: +To create an out-of-source build: 1. Clone the repository (or download and extract the tarball). 2. Create a build directory out-of-source. **Important**: The directory cannot be a subdirectory of the source folder. 3. `cd` into your build directory. 4. From your build directory, run `cmake` on the source directory to generate make files for the SDK: `cmake `. 5. After you've successfully run `cmake`, you should see the following message: `-- Please fill /Integration/AlexaClientSDKConfig.json before you execute integration tests.`. Open `Integration/AlexaClientSDKConfig.json` with your favorite text editor and fill in your product information. -6. From the build directory, run `make` to build the SDK. - -### Build for macOS - -Building for macOS requires some additional setup. Specifically, you need to ensure that you are running the latest version of cURL and that cURL is linked to nghttp2 (the default installation does not). +6. From the build directory, run `make` to build the SDK. -To recompile cURL, follow these instructions: +### Application Settings -1. Install [Homebrew](http://brew.sh/), if you haven't done so already. -2. Install cURL with HTTP2 support: -`brew install curl --with-nghttp2` -3. Force cURL to explicitly link to the updated binary: -`brew link curl --force` -4. Close and reopen terminal. -5. Confirm version and source with this command: -`brew info curl` +The SDK will require a configuration JSON file, an example of which is located in `Integration/AlexaClientSDKConfig.json`. The contents of the JSON should be populated with your product information (which you got from the developer portal when registering a product and creating a security profile), and the location of your database and sound files. This JSON file is required for the integration tests to work properly, as well as for the Sample App. -To create an out-of-source build for macOS: +The layout of the file is as follows: -1. Clone the repository (or download and extract the tarball). -2. Create a build directory out-of-source. **Important**: The directory cannot be a subdirectory of the source folder. -3. `cd` into your build directory. -4. From your build directory, run `cmake` on the source directory to generate make files for the SDK: `cmake `. -5. After you've successfully run `cmake`, you should see the following message: `-- Please fill /Integration/AlexaClientSDKConfig.json before you execute integration tests.`. Open `Integration/AlexaClientSDKConfig.json` with your favorite text editor and fill in your product information. -6. From the build directory, run `make` to build the SDK. +```json +{ + "authDelegate":{ + "deviceTypeId":"", + "clientId":"", + "clientSecret":"", + "deviceSerialNumber":"" + }, + "alertsCapabilityAgent": { + "databaseFilePath":"//", + "alarmSoundFilePath":"//alarm_normal.mp3", + "alarmShortSoundFilePath":"//alarm_short.wav", + "timerSoundFilePath":"//timer_normal.mp3", + "timerShortSoundFilePath":"//timer_short.wav" + } +} +``` +**NOTE**: The `deviceSerialNumber` is a unique identifier that you create. It is **not** provided by Amazon. +**NOTE**: Audio assets included in this repository are licensed as "Alexa Materials" under the [Alexa Voice +Service Agreement](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/support/terms-and-agreements). ## Run AuthServer @@ -297,7 +282,7 @@ You should see a message that indicates the server is running. ## Run Unit Tests -Unit tests for the Alexa Client SDK use the [Google Test](https://github.com/google/googletest) framework. Ensure that the [Google Test](https://github.com/google/googletest) is installed, then run the following command: +Unit tests for the AVS Device SDK for C++ use the [Google Test](https://github.com/google/googletest) framework. Ensure that the [Google Test](https://github.com/google/googletest) is installed, then run the following command: `make all test` Ensure that all tests are passed before you begin integration testing. @@ -347,208 +332,57 @@ Before you run the sample app, it's important to note that the application takes Navigate to the `SampleApp/src` folder from your build directory. Then run this command: ``` -TZ=UTC ./SampleApp +TZ=UTC ./SampleApp ``` -**Note**: Logging is currently disabled in the Sample App. We plan on updating the Sample App to allow the user to set logging levels when running the app, but at the moment, to turn on logging for the Sample App, the following lines (101-102) in SampleApp/src/main.cpp will need to be commented: -``` -alexaClientSDK::avsCommon::utils::logger::ConsoleLogger::instance().setLevel( -alexaClientSDK::avsCommon::utils::logger::Level::NONE); -``` -Alternatively, you can change the `NONE` to the desired level of logging. -**Note**: Enabling logging *might* cause Sample App messages and logging messages to become interwoven. +**Note**: The following logging levels are supported with `DEBUG9` providing the highest and `CRITICAL` the lowest level of logging: `DEBUG9`, `DEBUG8`, `DEBUG7`, `DEBUG6`, `DEBUG5`, `DEBUG4`, `DEBUG3`, `DEBUG2`, `DEBUG1`, `DEBUG0`, `INFO`, `WARN`, `ERROR`, and `CRITICAL`. -## Alexa Client SDK API Documentation +**Note**: The user must wait several seconds after starting up the sample app before the sample app is properly usable. -To build the Alexa Client SDK API documentation, run this command from your build directory: `make doc`. +## AVS Device SDK for C++ API Documentation + +To build API documentation locally, run this command from your build directory: `make doc`. ## Resources and Guides +* [How To Setup Your Raspberry Pi Development Environment](https://github.com/alexa/alexa-client-sdk/wiki/How-to-build-your-raspberrypi-development-environment) +* [How To Build libcurl with nghttp2 on macOS](https://github.com/alexa/alexa-client-sdk/wiki/How-to-build-libcurl-with-nghttp2-for-macos) * [Step-by-step instructions to optimize libcurl for size in `*nix` systems](https://github.com/alexa/alexa-client-sdk/wiki/optimize-libcurl). * [Step-by-step instructions to build libcurl with mbed TLS and nghttp2 for `*nix` systems](https://github.com/alexa/alexa-client-sdk/wiki/build-libcurl-with-mbed-TLS-and-nghttp2). -## Appendix A: Memory Profile - -This appendix provides the memory profiles for various modules of the Alexa Client SDK. The numbers were observed running integration tests on a machine running Ubuntu 16.04.2 LTS. +## Appendix A: Runtime Configuration of path to CA Certificates -| Module | Source Code Size (Bytes) | Library Size RELEASE Build (libxxx.so) (Bytes) | Library Size MINSIZEREL Build (libxxx.so) (Bytes) | -|--------|--------------------------|------------------------------------------------|---------------------------------------------------| -| ACL | 356 KB | 250 KB | 239 KB | -| ADSL | 224 KB | 175 KB | 159 KB | -| AFML | 80 KB | 133 KB | 126 KB | -| ContextManager | 84 KB | 122 KB | 116 KB | -| AIP | 184 KB | 340 KB | 348 KB | -| SpeechSynthesizer | 120 KB | 311 KB | 321 KB | -| AVSCommon | 772 KB | 252 KB | 228 KB | -| AVSUtils | 332 KB | 167 KB | 133 KB | -| Total | 2152 KB | 1750 KB | 1670 KB | - -**Runtime Memory** - -Unique size set (USS) and proportional size set (PSS) were measured by SMEM while integration tests were run. - -| Runtime Memory | Average USS | Max USS (Bytes) | Average PSS | Max PSS (Bytes) | -|----------------|-------------|-----------------|-------------|-----------------| -| ACL | 8 MB | 15 MB | 8 MB | 16 MB | -| ADSL + ACL | 8 MB | 20 MB | 9 MB | 21 MB | -| AIP | 9 MB | 12 MB | 9 MB | 13 MB | -| ** SpeechSynthesizer | 11 MB | 18 MB | 12 MB | 20 MB | - -** This test was run using the GStreamer-based MediaPlayer for audio playback. - -**Definitions** - -* **USS**: The amount of memory that is private to the process and not shared with any other processes. -* **PSS**: The amount of memory shared with other processes; divided by the number of processes sharing each page. - -## Appendix B: Directive Lifecycle Diagram - -![Directive Lifecycle](https://images-na.ssl-images-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-voice-service/docs/avs-directive-lifecycle.png) - -## Appendix C: Runtime Configuration of path to CA Certificates - -By default libcurl is built with paths to a CA bundle and a directory containing CA certificates. You can direct the Alexa Client SDK to configure libcurl to use an additional path to directories containing CA certificates via the [CURLOPT_CAPATH](https://curl.haxx.se/libcurl/c/CURLOPT_CAPATH.html) setting. This is done by adding a `"libcurlUtils/CURLOPT_CAPATH"` entry to the `AlexaClientSDKConfig.json` file. Here is an example: +By default libcurl is built with paths to a CA bundle and a directory containing CA certificates. You can direct the AVS Device SDK for C++ to configure libcurl to use an additional path to directories containing CA certificates via the [CURLOPT_CAPATH](https://curl.haxx.se/libcurl/c/CURLOPT_CAPATH.html) setting. This is done by adding a `"libcurlUtils/CURLOPT_CAPATH"` entry to the `AlexaClientSDKConfig.json` file. Here is an example: ``` { -"authDelegate" : { -"clientId" : "INSERT_YOUR_CLIENT_ID_HERE", -"refreshToken" : "INSERT_YOUR_REFRESH_TOKEN_HERE", -"clientSecret" : "INSERT_YOUR_CLIENT_SECRET_HERE" -}, -"libcurlUtils" : { -"CURLOPT_CAPATH" : "INSERT_YOUR_CA_CERTIFICATE_PATH_HERE" -} + "authDelegate" : { + "clientId" : "INSERT_YOUR_CLIENT_ID_HERE", + "refreshToken" : "INSERT_YOUR_REFRESH_TOKEN_HERE", + "clientSecret" : "INSERT_YOUR_CLIENT_SECRET_HERE" + }, + "libcurlUtils" : { + "CURLOPT_CAPATH" : "INSERT_YOUR_CA_CERTIFICATE_PATH_HERE" + } } ``` **Note** If you want to assure that libcurl is *only* using CA certificates from this path you may need to reconfigure libcurl with the `--without-ca-bundle` and `--without-ca-path` options and rebuild it to suppress the default paths. See [The libcurl documention](https://curl.haxx.se/docs/sslcerts.html) for more information. -## Release Notes - -v0.6 released 7/14/2017: - -* Added a sample app that leverages the SDK. -* Added an implementation of the `Alerts` capability agent. -* Added the `DefaultClient` class. -* Added the following classes to support directives and events in the [`Systems` interface](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/system): `StateSynchronizer`, `EndpointHandler`, and `ExceptionEncounteredSender`. -* Added unit tests for `ACL`. -* Updated `MediaPlayer` to play local files given an `std::istream`. -* Changed build configuration from `Debug` to `Release`. -* Removed `DeprecatedLogger` class. -* Known Issues: - * MediaPlayer: Our `GStreamer` based implementation of `MediaPlayer` is not fully robust, and may result in fatal runtime errors, under the following conditions: - * Attempting to play multiple simultaneous audio streams - * Calling `MediaPlayer::play()` and `MediaPlayer::stop()` when the MediaPlayer is already playing or stopped, respectively. - * Other miscellaneous issues, which will be addressed in the near future - * `AlertsCapabilityAgent`: - * This component has been temporarily simplified to work around the known `MediaPlayer` issues mentioned above - * Fully satisfies the AVS specification except for sending retrospective Events, for example, sending `AlertStarted` for an Alert which rendered when there was no Internet connection - * This component is not fully thread-safe, however, this will be addressed shortly - * Alerts currently run indefinitely until stopped manually by the user. This will be addressed shortly by having a timeout value for an alert to stop playing. - * Alerts do not play in the background when Alexa is speaking, but will continue playing after Alexa stops speaking. - * `Sample App`: - * Without the refresh token being filled out in the JSON file, the sample app crashes on start up. - * Any connection loss during the `Listening` state keeps the app stuck in that state, unless the ongoing interaction is manually stopped by the user. - * At the end of a shopping list with more than 5 items, the interaction in which Alexa asks the user if he/she would like to hear more does not finish properly. - * `Tests`: - * `SpeechSynthesizer` unit tests hang on some older versions of GCC due to a tear down issue in the test suite - * Intermittent Alerts integration test failures caused by rigidness in expected behavior in the tests - -v0.5 released 6/23/2017: - -* Updated most SDK components to use new logging abstraction. -* Added a `getConfiguration()` method to `DirectiveHandlerInterface` to register Capability Agents with Directive Sequencer. -* Added ACL stream processing with Pause and redrive. -* Removed the dependency of ACL Library on `Authdelegate`. -* Added an interface to allow ACL to Add/Remove `ConnectionStatusObserverInterface`. -* Fixed compile errors in KittAi, DirectiveHandler and compiler warnings in AIP test. -* Corrected formatting of code in many files. -* Fixes for the following Github issues: -* [MessageRequest callbacks never triggered if disconnected](https://github.com/alexa/alexa-client-sdk/issues/21) -* [AttachmentReader::read() returns ReadStatus::CLOSED if an AttachmentWriter has not been created yet](https://github.com/alexa/alexa-client-sdk/issues/25) - -v0.4.1 released 6/9/2017: - -* Implemented Sensory wake word detector functionality -* Removed the need for a `std::recursive_mutex` in `MessageRouter` -* Added AIP unit test -* Added `handleDirectiveImmediately` functionality to `SpeechSynthesizer` -* Added memory profiles for: -* AIP -* SpeechSynthesizer -* ContextManager -* AVSUtils -* AVSCommon -* Bug fix for `MessageRouterTest` aborting intermittently -* Bug fix for `MultipartParser.h` compiler warning -* Suppression of sensitive log data even in debug builds. Use cmake parameter -DACSDK_EMIT_SENSITIVE_LOGS=ON to allow logging of sensitive information in DEBUG builds -* Fix crash in ACL when attempting to use more than 10 streams -* Updated MediaPlayer to use `autoaudiosink` instead of requiring `pulseaudio` -* Updated MediaPlayer build to suppport local builds of GStreamer -* Fixes for the following Github issues: -* [MessageRouter::send() does not take the m_connectionMutex](https://github.com/alexa/alexa-client-sdk/issues/5) -* [MessageRouter::disconnectAllTransportsLocked flow leads to erase while iterating transports vector](https://github.com/alexa/alexa-client-sdk/issues/8) -* [Build errors when building with KittAi enabled](https://github.com/alexa/alexa-client-sdk/issues/9) -* [HTTP2Transport race may lead to deadlock](https://github.com/alexa/alexa-client-sdk/issues/10) -* [Crash in HTTP2Transport::cleanupFinishedStreams()](https://github.com/alexa/alexa-client-sdk/issues/17) -* [The attachment writer interface should take a `const void*` instead of `void*`](https://github.com/alexa/alexa-client-sdk/issues/24) - -v0.4 updated 5/31/2017: - -* Added `AuthServer`, an authorization server implementation used to retrieve refresh tokens from LWA. - -v0.4 release 5/24/2017: - -* Added the `SpeechSynthesizer`, an implementation of the `SpeechRecognizer` capability agent. -* Implemented a reference `MediaPlayer` based on [GStreamer](https://gstreamer.freedesktop.org/) for audio playback. -* Added the `MediaPlayerInterface` that allows you to implement your own media player. -* Updated `ACL` to support asynchronous receipt of audio attachments from AVS. -* Bug Fixes: -* Some intermittent unit test failures were fixed. -* Known Issues: -* `ACL`'s asynchronous receipt of audio attachments may manage resources poorly in scenarios where attachments are received but not consumed. -* When an `AttachmentReader` does not deliver data for prolonged periods `MediaPlayer` may not resume playing the delayed audio. - -v0.3 released 5/17/2017: - -* Added the `CapabilityAgent` base class that is used to build capability agent implementations. -* Added the `ContextManager` class that allows multiple Capability Agents to store and access state. These events include `context`, which is used to communicate the state of each capability agent to AVS: -* [`Recognize`](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/speechrecognizer#recognize) -* [`PlayCommandIssued`](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/playbackcontroller#playcommandissued) -* [`PauseCommandIssued`](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/playbackcontroller#pausecommandissued) -* [`NextCommandIssued`](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/playbackcontroller#nextcommandissued) -* [`PreviousCommandIssued`](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/playbackcontroller#previouscommandissued) -* [`SynchronizeState`](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/system#synchronizestate) -* [`ExceptionEncountered`](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/system#exceptionencountered) -* Implemented the `SharedDataStream` (SDS) to asynchronously communicate data between a local reader and writer. -* Added `AudioInputProcessor` (AIP), an implementation of a `SpeechRecognizer` capability agent. -* Added the WakeWord Detector (WWD), which recognizes keywords in audio streams. v0.3 implements a wrapper for KITT.ai. -* Added a new implementation of `AttachmentManager` and associated classes for use with SDS. -* Updated the `ACL` to support asynchronously sending audio to AVS. - -v0.2.1 released 5/3/2017: -* Replaced the configuration file `AuthDelegate.config` with `AlexaClientSDKConfig.json`. -* Added the ability to specify a `CURLOPT_CAPATH` value to be used when libcurl is used by ACL and AuthDelegate. See [**Appendix C**](#appendix-c-runtime-configuration-of-path-to-ca-certificates) for details. -* Changes to ADSL interfaces: -The v0.2 interface for registering directive handlers (`DirectiveSequencer::setDirectiveHandlers()`) was problematic because it canceled the ongoing processing of directives and dropped further directives until it completed. The revised API makes the operation immediate without canceling or dropping any handling. However, it does create the possibility that `DirectiveHandlerInterface` methods `preHandleDirective()` and `handleDirective()` may be called on different handlers for the same directive. -* `DirectiveSequencerInterface::setDirectiveHandlers()` was replaced by `addDirectiveHandlers()` and `removeDirectiveHandlers()`. -* `DirectiveHandlerInterface::shutdown()` was replaced with `onDeregistered()`. -* `DirectiveHandlerInterface::preHandleDirective()` now takes a `std::unique_ptr` instead of a `std::shared_ptr` to `DirectiveHandlerResultInterface`. -* `DirectiveHandlerInterface::handleDirective()` now returns a bool indicating if the handler recognizes the `messageId`. -* Bug fixes: -* ACL and AuthDelegate now require TLSv1.2. -* `onDirective()` now sends `ExceptionEncountered` for unhandled directives. -* `DirectiveSequencer::shutdown()` no longer sends `ExceptionEncountered()` for queued directives. - -v0.2 updated 3/27/2017: -* Added memory profiling for ACL and ADSL. See [**Appendix A**](#appendix-a-mempry-profile). -* Added command to build API documentation. - -v0.2 released 3/9/2017: -* Alexa Client SDK v0.2 released. -* Architecture diagram has been updated to include the ADSL and AMFL. -* CMake build types and options have been updated. -* New documentation for libcurl optimization included. - -v0.1 released 2/10/2017: -* Alexa Client SDK v0.1 released. +## Release Notes + +**Note**: Features, updates, and resolved issues from previous releases are available to view in CHANGELOG.md. + +v1.0.0 released 8/7/2017: +* Features + * Native components for the following capability agents are included in this release: `Alerts`, `AudioPlayer`, `SpeechRecognizer`, `SpeechSynthesizer`, and `System` + * Supports iHeartRadio + * Includes a sample application to demonstrate interactions with AVS +* Known Issues + * Native components for the following capability agents are **not** included in this release: `PlaybackController`, `Speaker`, `Settings`, `TemplateRuntime`, and `Notifications` + * Amazon Music, TuneIn, and SiriusXM are not supported in v1.0 + * The `AlertsCapabilityAgent` satisfies the [AVS specification](https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/timers-and-alarms-conceptual-overview) except for sending retrospective events. For example, sending `AlertStarted` for an Alert which rendered when there was no internet connection. + * `ACL`'s asynchronous receipt of audio attachments may manage resources poorly in scenarios where attachments are received but not consumed. + * When an `AttachmentReader` does not deliver data for prolonged periods, `MediaPlayer` may not resume playing the delayed audio. + * Without the refresh token in the JSON file, the sample app crashes on start up. + * Any connection loss during the `Listening` state keeps the app stuck in this state, unless the ongoing interaction is manually stopped by the user. + * The user must wait several seconds after starting up the sample app before the sample app is properly usable. diff --git a/SampleApp/include/SampleApp/InteractionManager.h b/SampleApp/include/SampleApp/InteractionManager.h index 8c4e7925e5..921505f18c 100644 --- a/SampleApp/include/SampleApp/InteractionManager.h +++ b/SampleApp/include/SampleApp/InteractionManager.h @@ -20,7 +20,6 @@ #include -#include #include #include "PortAudioMicrophoneWrapper.h" @@ -33,7 +32,7 @@ namespace sampleApp { * This class manages most of the user interaction by taking in commands and notifying the DefaultClient and the * userInterface (the view) accordingly. */ -class InteractionManager : public avsCommon::sdkInterfaces::DialogUXStateObserverInterface { +class InteractionManager { public: /** * Constructor. @@ -57,8 +56,6 @@ class InteractionManager : public avsCommon::sdkInterfaces::DialogUXStateObserve */ void help(); - void onDialogUXStateChanged(avsCommon::sdkInterfaces::DialogUXStateObserverInterface::DialogUXState state) override; - /** * Toggles the microphone state if the Sample App was built with wakeword. When the microphone is turned off, the * app enters a privacy mode in which it stops recording audio data from the microphone, thus disabling Alexa waking diff --git a/SampleApp/src/InteractionManager.cpp b/SampleApp/src/InteractionManager.cpp index 29ef34523b..2e5100ce19 100644 --- a/SampleApp/src/InteractionManager.cpp +++ b/SampleApp/src/InteractionManager.cpp @@ -35,11 +35,8 @@ InteractionManager::InteractionManager( m_wakeWordAudioProvider{wakeWordAudioProvider}, m_isHoldOccurring{false}, m_isTapOccurring{false}, - m_isMicOn{false} { - if (m_wakeWordAudioProvider) { - m_isMicOn = true; + m_isMicOn{true} { m_micWrapper->startStreamingMicrophoneData(); - } }; void InteractionManager::begin() { @@ -59,26 +56,6 @@ void InteractionManager::help() { ); } -void InteractionManager::onDialogUXStateChanged( - avsCommon::sdkInterfaces::DialogUXStateObserverInterface::DialogUXState state) { - m_executor.submit( - [this, state] () { - /* - * This is an optimization that stops the microphone from continuously streaming audio data into the buffer - * when a tap to talk recognize finishes. This isn't strictly necessary, as the SDK will not consume the - * audio data when not in the middle of an active recognition. - */ - if (avsCommon::sdkInterfaces::DialogUXStateObserverInterface::DialogUXState::IDLE == state && - m_isTapOccurring) { - m_isTapOccurring = false; - if (!m_wakeWordAudioProvider || (m_wakeWordAudioProvider && !m_isMicOn)) { - m_micWrapper->stopStreamingMicrophoneData(); - } - } - } - ); -} - void InteractionManager::microphoneToggle() { m_executor.submit( [this] () { @@ -101,17 +78,16 @@ void InteractionManager::microphoneToggle() { void InteractionManager::holdToggled() { m_executor.submit( [this] () { + if (!m_isMicOn) { + return; + } if (!m_isHoldOccurring) { if (m_client->notifyOfHoldToTalkStart(m_holdToTalkAudioProvider).get()) { m_isHoldOccurring = true; - m_micWrapper->startStreamingMicrophoneData(); } } else { m_isHoldOccurring = false; m_client->notifyOfHoldToTalkEnd(); - if (!m_wakeWordAudioProvider || (m_wakeWordAudioProvider && !m_isMicOn)) { - m_micWrapper->stopStreamingMicrophoneData(); - } } } ); @@ -120,9 +96,11 @@ void InteractionManager::holdToggled() { void InteractionManager::tap() { m_executor.submit( [this] () { + if (!m_isMicOn) { + return; + } if (m_client->notifyOfTapToTalk(m_tapToTalkAudioProvider).get()) { m_isTapOccurring = true; - m_micWrapper->startStreamingMicrophoneData(); } } ); diff --git a/SampleApp/src/UserInputManager.cpp b/SampleApp/src/UserInputManager.cpp index e1e7f8d666..8938343bf0 100644 --- a/SampleApp/src/UserInputManager.cpp +++ b/SampleApp/src/UserInputManager.cpp @@ -18,7 +18,7 @@ #include "SampleApp/UserInputManager.h" #include "SampleApp/ConsolePrinter.h" -#include +#include namespace alexaClientSDK { namespace sampleApp { @@ -50,6 +50,7 @@ void UserInputManager::run() { while(true) { char x; std::cin >> x; + x = ::tolower(x); if (x == QUIT) { return; } else if (x == INFO) { diff --git a/SampleApp/src/main.cpp b/SampleApp/src/main.cpp index e28d102668..8109a8c153 100644 --- a/SampleApp/src/main.cpp +++ b/SampleApp/src/main.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include @@ -56,6 +58,33 @@ static const float KITT_AI_AUDIO_GAIN = 2.0; static const bool KITT_AI_APPLY_FRONT_END_PROCESSING = true; #endif +/// A set of all log levels. +static const std::set allLevels = { + alexaClientSDK::avsCommon::utils::logger::Level::DEBUG9, + alexaClientSDK::avsCommon::utils::logger::Level::DEBUG8, + alexaClientSDK::avsCommon::utils::logger::Level::DEBUG7, + alexaClientSDK::avsCommon::utils::logger::Level::DEBUG6, + alexaClientSDK::avsCommon::utils::logger::Level::DEBUG5, + alexaClientSDK::avsCommon::utils::logger::Level::DEBUG4, + alexaClientSDK::avsCommon::utils::logger::Level::DEBUG3, + alexaClientSDK::avsCommon::utils::logger::Level::DEBUG2, + alexaClientSDK::avsCommon::utils::logger::Level::DEBUG1, + alexaClientSDK::avsCommon::utils::logger::Level::DEBUG0, + alexaClientSDK::avsCommon::utils::logger::Level::INFO, + alexaClientSDK::avsCommon::utils::logger::Level::WARN, + alexaClientSDK::avsCommon::utils::logger::Level::ERROR, + alexaClientSDK::avsCommon::utils::logger::Level::CRITICAL, + alexaClientSDK::avsCommon::utils::logger::Level::NONE +}; + +/** + * Gets a log level consumable by the SDK based on the user input string for log level. + * + * @param userInputLogLevel The string to be parsed into a log level. + * @return The log level. This will default to NONE if the input string is not properly parsable. + */ +alexaClientSDK::avsCommon::utils::logger::Level getLogLevelFromUserInput(std::string userInputLogLevel); + /** * This serves as the starting point for the application. The main activities here are setting up authorization, an * output media player, input audio streams, and the DefaultClient. @@ -63,21 +92,32 @@ static const bool KITT_AI_APPLY_FRONT_END_PROCESSING = true; int main(int argc, char **argv) { std::string pathToConfig; std::string pathToInputFolder; + alexaClientSDK::avsCommon::utils::logger::Level logLevel = alexaClientSDK::avsCommon::utils::logger::Level::NONE; #if defined(KWD_KITTAI) || defined(KWD_SENSORY) if (argc < 3) { alexaClientSDK::sampleApp::ConsolePrinter::simplePrint( - "USAGE: ./SampleApp "); + "USAGE: " + + std::string(argv[0]) + + " [log_level]"); return EXIT_FAILURE; } else { pathToInputFolder = std::string(argv[2]); + if (4 == argc) { + std::string inputLogLevel = std::string(argv[3]); + logLevel = getLogLevelFromUserInput(inputLogLevel); + } } #else if (argc < 2) { alexaClientSDK::sampleApp::ConsolePrinter::simplePrint( - "USAGE: ./SampleApp "); + "USAGE: " + std::string(argv[0]) + " [log_level]"); return EXIT_FAILURE; } + if (3 == argc) { + std::string inputLogLevel = std::string(argv[2]); + logLevel = getLogLevelFromUserInput(inputLogLevel); + } #endif pathToConfig = std::string(argv[1]); @@ -96,10 +136,24 @@ int main(int argc, char **argv) { return EXIT_FAILURE; } + if (alexaClientSDK::avsCommon::utils::logger::Level::UNKNOWN == logLevel) { + alexaClientSDK::sampleApp::ConsolePrinter::simplePrint("Unknown log level input!"); + alexaClientSDK::sampleApp::ConsolePrinter::simplePrint("Possible log level options are: "); + for (auto it = allLevels.begin(); + it != allLevels.end(); + ++it) { + alexaClientSDK::sampleApp::ConsolePrinter::simplePrint( + alexaClientSDK::avsCommon::utils::logger::convertLevelToName(*it) + ); + } + return EXIT_FAILURE; + } + // TODO: ACSDK-362/386 Find a way to log and print output at the same time that the messages don't get scrambled up // Setting logging to none so that the application may print its own output. - alexaClientSDK::avsCommon::utils::logger::ConsoleLogger::instance().setLevel( - alexaClientSDK::avsCommon::utils::logger::Level::NONE); + alexaClientSDK::sampleApp::ConsolePrinter::simplePrint( + "Running app with log level: " + alexaClientSDK::avsCommon::utils::logger::convertLevelToName(logLevel)); + alexaClientSDK::avsCommon::utils::logger::ConsoleLogger::instance().setLevel(logLevel); /* * Creating the media players. Here, the default GStreamer based MediaPlayer is being created. However, any @@ -107,7 +161,13 @@ int main(int argc, char **argv) { */ auto speakMediaPlayer = alexaClientSDK::mediaPlayer::MediaPlayer::create(); if (!speakMediaPlayer) { - alexaClientSDK::sampleApp::ConsolePrinter::simplePrint("Failed to create media player!"); + alexaClientSDK::sampleApp::ConsolePrinter::simplePrint("Failed to create media player for speech!"); + return EXIT_FAILURE; + } + + auto audioMediaPlayer = alexaClientSDK::mediaPlayer::MediaPlayer::create(); + if (!audioMediaPlayer) { + alexaClientSDK::sampleApp::ConsolePrinter::simplePrint("Failed to create media player for content!"); return EXIT_FAILURE; } @@ -147,6 +207,7 @@ int main(int argc, char **argv) { std::shared_ptr client = alexaClientSDK::defaultClient::DefaultClient::create( speakMediaPlayer, + audioMediaPlayer, alertsMediaPlayer, authDelegate, alertStorage, @@ -230,6 +291,9 @@ int main(int argc, char **argv) { std::shared_ptr micWrapper = alexaClientSDK::sampleApp::PortAudioMicrophoneWrapper::create(sharedDataStream); + if (!micWrapper) { + return EXIT_FAILURE; + } // Creating wake word audio provider, if necessary #ifdef KWD @@ -287,11 +351,6 @@ int main(int argc, char **argv) { holdToTalkAudioProvider, tapToTalkAudioProvider); #endif - /* - * Adding the interaction manager as an dialog state observer here so that it may know when to stop streaming - * microphone data, specifically for tap to talk interactions. - */ - client->addAlexaDialogStateObserver(interactionManager); // Creating the input observer and running it. auto inputManager = alexaClientSDK::sampleApp::UserInputManager::create(interactionManager); @@ -304,4 +363,9 @@ int main(int argc, char **argv) { inputManager->run(); return EXIT_SUCCESS; -} \ No newline at end of file +} + +alexaClientSDK::avsCommon::utils::logger::Level getLogLevelFromUserInput(std::string userInputLogLevel) { + std::transform(userInputLogLevel.begin(), userInputLogLevel.end(), userInputLogLevel.begin(), ::toupper); + return alexaClientSDK::avsCommon::utils::logger::convertNameToLevel(userInputLogLevel); +} diff --git a/build/BuildDefaults.cmake b/build/BuildDefaults.cmake index b256ada7ec..77e35e8b67 100644 --- a/build/BuildDefaults.cmake +++ b/build/BuildDefaults.cmake @@ -21,6 +21,9 @@ include(Logger) # Setup keyword requirement variables. include(KeywordDetector) +# Setup playlist parser variables. +include(PlaylistParser) + # Setup media player variables. include(MediaPlayer) diff --git a/build/cmake/PlaylistParser.cmake b/build/cmake/PlaylistParser.cmake new file mode 100644 index 0000000000..48b4de05b4 --- /dev/null +++ b/build/cmake/PlaylistParser.cmake @@ -0,0 +1,16 @@ +# +# Setup the PlaylistParser build. +# +# To build the Totem-PlParser based PlaylistParser, run the following command, +# cmake -DTOTEM_PLPARSER=ON. +# +# + +option(TOTEM_PLPARSER "Enable Totem-Pl-Parser based playlist parser." OFF) + +set(PKG_CONFIG_USE_CMAKE_PREFIX_PATH ON) +if(TOTEM_PLPARSER) + find_package(PkgConfig) + pkg_check_modules(TOTEM REQUIRED totem-plparser>=3.10) + add_definitions(-DTOTEM_PLPARSER) +endif() \ No newline at end of file