avs-device-sdk/MediaPlayer/test/MediaPlayerTest.cpp

497 lines
19 KiB
C++

/*
* MediaPlayerTest.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 <algorithm>
#include <chrono>
#include <condition_variable>
#include <fstream>
#include <memory>
#include <mutex>
#include <thread>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <AVSCommon/Utils/Logger/Logger.h>
#include <AVSCommon/Utils/Memory/Memory.h>
#include "MediaPlayer/MediaPlayer.h"
namespace alexaClientSDK {
namespace mediaPlayer {
namespace test {
using namespace avsCommon::utils::mediaPlayer;
using namespace avsCommon::avs::attachment;
using namespace avsCommon::utils::memory;
using namespace ::testing;
/// String to identify log entries originating from this file.
static const std::string TAG("MediaPlayerTest");
/**
* 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;
/// MP3 test file path.
static const std::string MP3_FILE_PATH("/fox_dog.mp3");
/**
* Mock AttachmentReader.
*/
class MockAttachmentReader : public AttachmentReader {
public:
/**
* Constructor.
*
* @param iterations The number of times this AttachmentReader will (re)read the input file before @c read
* will return a @c CLOSED status.
* @param receiveSizes An vector of sizes (in bytes) that this @c AttachmentReceiver will simulate receiving.
* Each successive element in the vector corresponds to a successive 100 millisecond interval starting from
* the time this @c MockAttachmentReader was created.
*/
MockAttachmentReader(int iterations = 1, std::vector<size_t> receiveSizes = {std::numeric_limits<size_t>::max()});
size_t read(
void* buf, std::size_t numBytes, ReadStatus* readStatus, std::chrono::milliseconds timeoutMs) override;
void close(ClosePoint closePoint) override;
/**
* Receive bytes from the test file.
*
* @param buf The buffer to receive the bytes.
* @param size The number of bytes to receive.
* @return The number of bytes received.
*/
size_t receiveBytes(char* buf, std::size_t size);
/// The number of iterations of reading the input file that are left before this reader returns closed.
int m_iterationsLeft;
/**
* The total number of bytes that are supposed to have been received (and made available) by this
* @c AttachmentReader at 100 millisecond increments from @c m_startTime.
*/
std::vector<size_t> m_receiveTotals;
/// The start of time for reading from this AttachmentReader.
std::chrono::steady_clock::time_point m_startTime;
/// The number of bytes returned so far by @c read().
size_t m_totalRead;
/// The current ifstream (if any) from which to read the attachment.
std::unique_ptr<std::ifstream> m_stream;
};
MockAttachmentReader::MockAttachmentReader(int iterations, std::vector<size_t> receiveSizes) :
m_iterationsLeft{iterations},
m_startTime(std::chrono::steady_clock::now()),
m_totalRead{0} {
// Convert human friendly vector of received sizes in to a vector of received totals.
EXPECT_GT(receiveSizes.size(), 0U);
m_receiveTotals.reserve(receiveSizes.size());
size_t total = 0;
for (size_t ix = 0; ix < receiveSizes.size(); ix++) {
total += receiveSizes[ix];
m_receiveTotals.push_back(total);
}
EXPECT_EQ(m_receiveTotals.size(), receiveSizes.size());
}
size_t MockAttachmentReader::read(
void* buf, std::size_t numBytes, ReadStatus* readStatus, std::chrono::milliseconds timeoutMs) {
// Convert the current time in to an index in to m_receivedTotals (time since @c m_startTime
// divided by 100 millisecond intervals).
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_startTime);
auto index = static_cast<size_t>(elapsed.count() / 100);
index = std::min(index, m_receiveTotals.size() - 1);
// Use the index to find the total number of bytes received by the @c MockAttachmentReader so far. Then subtract
// the number of bytes consumed by @c read() so far to calculate the number of bytes available for @c read().
auto receivedTotal = m_receiveTotals[index];
EXPECT_LE(m_totalRead, receivedTotal);
auto available = receivedTotal - m_totalRead;
// Use the avaialable number of bytes to calculate how many bytes @c read() should return and the status
// that should accompany them. Also perform the actual read in to the output buffer.
size_t result = 0;
auto status = ReadStatus::ERROR_INTERNAL;
if (available > 0) {
auto sizeToRead = std::min(available, numBytes);
result = receiveBytes(static_cast<char *>(buf), sizeToRead);
if (result > 0) {
m_totalRead += result;
status = (result == numBytes) ? ReadStatus::OK : ReadStatus::OK_WOULDBLOCK;
} else {
status = ReadStatus::CLOSED;
}
} else {
status = ReadStatus::OK_WOULDBLOCK;
}
if (readStatus) {
*readStatus = status;
}
return result;
}
void MockAttachmentReader::close(ClosePoint closePoint) {
if (m_stream) {
m_stream->close();
}
}
size_t MockAttachmentReader::receiveBytes(char* buf, std::size_t size) {
auto pos = buf;
auto end = buf + size;
while (pos < end) {
if (!m_stream || m_stream->eof()) {
if (m_iterationsLeft-- > 0) {
m_stream = make_unique<std::ifstream>(inputsDirPath + MP3_FILE_PATH);
EXPECT_TRUE(m_stream);
EXPECT_TRUE(m_stream->good());
} else {
break;
}
}
m_stream->read(pos, end - pos);
pos += m_stream->gcount();
}
auto result = pos - buf;
return result;
}
class MockPlayerObserver: public MediaPlayerObserverInterface {
public:
/**
* Destructor.
*/
~MockPlayerObserver() {};
void onPlaybackStarted() override;
void onPlaybackFinished() override;
void onPlaybackError(std::string error) override;
/**
* 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 waitForPlaybackStarted(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 waitForPlaybackFinished(const std::chrono::milliseconds duration = std::chrono::milliseconds(5000));
private:
/// Mutex to protect the flags @c m_playbackStarted and .@c m_playbackFinished.
std::mutex m_mutex;
/// Trigger to wake up m_wakePlaybackStarted calls.
std::condition_variable m_wakePlaybackStarted;
/// Trigger to wake up m_wakePlaybackStarted calls.
std::condition_variable m_wakePlaybackFinished;
/// 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;
};
void MockPlayerObserver::onPlaybackStarted() {
std::lock_guard<std::mutex> lock(m_mutex);
m_playbackStarted = true;
m_playbackFinished = false;
m_wakePlaybackStarted.notify_all();
}
void MockPlayerObserver::onPlaybackFinished() {
std::lock_guard<std::mutex> lock(m_mutex);
m_playbackFinished = true;
m_playbackStarted = false;
m_wakePlaybackFinished.notify_all();
}
void MockPlayerObserver::onPlaybackError(std::string error) {
ACSDK_ERROR(LX("onPlaybackError").d("error", error));
};
bool MockPlayerObserver::waitForPlaybackStarted(const std::chrono::milliseconds duration) {
std::unique_lock<std::mutex> lock(m_mutex);
if (!m_wakePlaybackStarted.wait_for(lock, duration, [this]() { return m_playbackStarted; } ))
{
return false;
}
return true;
}
bool MockPlayerObserver::waitForPlaybackFinished(const std::chrono::milliseconds duration) {
std::unique_lock<std::mutex> lock(m_mutex);
if (!m_wakePlaybackFinished.wait_for(lock, duration, [this]() { return m_playbackFinished; } ))
{
return false;
}
return true;
}
class MediaPlayerTest: public ::testing::Test{
public:
void SetUp() override;
/**
* Sets the audio source to play.
*
*/
void setAttachmentReaderSource(
int iterations = 1,
std::vector<size_t> receiveSizes = {std::numeric_limits<size_t>::max()});
/**
* Sets IStream source to play.
*
* @param repeat Whether to play the stream over and over until stopped.
*/
void setIStreamSource(bool repeat = false);
/// An instance of the @c MediaPlayer
std::shared_ptr<MediaPlayer> m_mediaPlayer;
/// An observer to whom the playback started and finished notifications need to be sent.
std::shared_ptr<MockPlayerObserver> m_playerObserver;
};
void MediaPlayerTest::SetUp() {
m_playerObserver = std::make_shared<MockPlayerObserver>();
m_mediaPlayer = MediaPlayer::create();
ASSERT_TRUE(m_mediaPlayer);
m_mediaPlayer->setObserver(m_playerObserver);
}
void MediaPlayerTest::setAttachmentReaderSource(int iterations, std::vector<size_t> receiveSizes) {
ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->setSource(
std::unique_ptr<AttachmentReader>(new MockAttachmentReader(iterations, receiveSizes))));
}
void MediaPlayerTest::setIStreamSource(bool repeat) {
ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->setSource(
make_unique<std::ifstream>(inputsDirPath + MP3_FILE_PATH), repeat));
}
/**
* 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.
*/
TEST_F(MediaPlayerTest, testStartPlayWaitForEnd) {
setAttachmentReaderSource();
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.
* Call @c play. The audio should play again from the beginning. Wait till the end.
*/
TEST_F(MediaPlayerTest, testStartPlayWaitForEndStartPlayAgain) {
setIStreamSource();
ASSERT_NE(MediaPlayerStatus::FAILURE,m_mediaPlayer->play());
ASSERT_TRUE(m_playerObserver->waitForPlaybackStarted());
ASSERT_TRUE(m_playerObserver->waitForPlaybackFinished());
setAttachmentReaderSource();
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 for a few
* seconds. Playback started notification should be received when the playback starts. Then call @c stop and expect
* the playback finished notification is received.
*/
TEST_F(MediaPlayerTest, testStopPlay) {
setIStreamSource(true);
ASSERT_NE(MediaPlayerStatus::FAILURE,m_mediaPlayer->play());
ASSERT_TRUE(m_playerObserver->waitForPlaybackStarted());
std::this_thread::sleep_for (std::chrono::seconds(5));
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 stop.
* and wait for the playback finished notification is received. Call @c play again.
*/
TEST_F(MediaPlayerTest, testStartPlayCallAfterStopPlay) {
setAttachmentReaderSource();
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->stop());
ASSERT_TRUE(m_playerObserver->waitForPlaybackFinished());
setAttachmentReaderSource();
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->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.
* Check the offset value. Then call @c stop and expect the playback finished notification is received.
* Call @c getOffsetInMilliseconds again. Check the offset value.
*/
TEST_F(MediaPlayerTest, testGetOffsetInMilliseconds) {
setAttachmentReaderSource();
ASSERT_NE(MediaPlayerStatus::FAILURE,m_mediaPlayer->play());
ASSERT_TRUE(m_playerObserver->waitForPlaybackStarted());
std::this_thread::sleep_for (std::chrono::seconds(1));
ASSERT_NE(-1, m_mediaPlayer->getOffsetInMilliseconds());
ASSERT_NE(MediaPlayerStatus::FAILURE,m_mediaPlayer->stop());
ASSERT_TRUE(m_playerObserver->waitForPlaybackFinished());
ASSERT_EQ(-1, m_mediaPlayer->getOffsetInMilliseconds());
}
/**
* Check playing two attachments back to back.
* 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.
* Check the offset value. Wait for playback to finish and expect the playback finished notification is received.
* Repeat the above for a new source.
*/
TEST_F(MediaPlayerTest, testPlayingTwoAttachments) {
setAttachmentReaderSource();
ASSERT_NE(MediaPlayerStatus::FAILURE,m_mediaPlayer->play());
ASSERT_TRUE(m_playerObserver->waitForPlaybackStarted());
std::this_thread::sleep_for (std::chrono::seconds(1));
ASSERT_NE(-1, m_mediaPlayer->getOffsetInMilliseconds());
ASSERT_TRUE(m_playerObserver->waitForPlaybackFinished());
setAttachmentReaderSource();
ASSERT_NE(MediaPlayerStatus::FAILURE,m_mediaPlayer->play());
ASSERT_TRUE(m_playerObserver->waitForPlaybackStarted());
std::this_thread::sleep_for (std::chrono::seconds(1));
ASSERT_NE(-1, m_mediaPlayer->getOffsetInMilliseconds());
std::this_thread::sleep_for (std::chrono::seconds(1));
ASSERT_NE(MediaPlayerStatus::FAILURE,m_mediaPlayer->stop());
ASSERT_TRUE(m_playerObserver->waitForPlaybackFinished());
}
/**
* Check playback of an attachment that is received sporadically. Playback started notification should be received
* when the playback starts. Wait for playback to finish and expect the playback finished notification is received.
* To a human ear the playback of this test is expected to sound reasonably smooth.
*/
TEST_F(MediaPlayerTest, testUnsteadyReads) {
setAttachmentReaderSource(
3, {
// Sporadic receive sizes averaging out to about 6000 bytes per second.
// Each element corresponds to a 100 millisecond time interval, so each
// row of 10 corresponds to a second's worth of sizes of data.
4000, 1000, 500, 500, 0, 0, 0, 0, 0, 0,
0, 0, 0, 500, 0, 500, 0, 1000, 0, 4000,
0, 100, 100, 100, 100, 100, 0, 2500, 0, 3000,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 6000, 0, 0, 0, 6000,
0, 0, 0, 3000, 0, 0, 0, 0, 0, 3000,
0, 2000, 0, 0, 2000, 0, 0, 0, 2000, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 12000,
0, 0, 0, 1000, 0, 0, 0, 1000, 0, 1000,
0, 0, 0, 0, 3000, 0, 0, 0, 0, 6000
});
ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->play());
ASSERT_TRUE(m_playerObserver->waitForPlaybackStarted());
ASSERT_TRUE(m_playerObserver->waitForPlaybackFinished(std::chrono::milliseconds(15000)));
}
// TODO: ACSDK-300 This test fails frequently on the Raspberry Pi platform.
#ifdef RESOLVED_ACSDK_300
/**
* Check playback of an attachment whose receipt is interrupted for about 3 seconds. Playback started notification
* should be received when the playback starts. Wait for playback to finish and expect the playback finished
* notification is received. To a human ear the playback of this test is expected to sound reasonably smooth
* initially, then be interrupted for a few seconds, and then continue fairly smoothly.
*/
TEST_F(MediaPlayerTest, testRecoveryFromPausedReads) {
setAttachmentReaderSource(
3, {
// Receive sizes averaging out to 6000 bytes per second with a 3 second gap.
// Each element corresponds to a 100 millisecond time interval, so each
// row of 10 corresponds to a second's worth of sizes of data.
0, 0, 0, 0, 0, 0, 0, 0, 0, 6000,
0, 0, 0, 0, 0, 0, 0, 0, 0, 6000,
0, 0, 0, 0, 0, 0, 0, 0, 0, 6000,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 18000,
0, 0, 0, 0, 0, 0, 0, 0, 0, 6000,
0, 0, 0, 0, 0, 0, 0, 0, 0, 6000,
0, 0, 0, 0, 0, 0, 0, 0, 0, 6000,
0, 0, 0, 0, 0, 0, 0, 0, 0, 6000
});
ASSERT_NE(MediaPlayerStatus::FAILURE, m_mediaPlayer->play());
ASSERT_TRUE(m_playerObserver->waitForPlaybackStarted());
ASSERT_TRUE(m_playerObserver->waitForPlaybackFinished(std::chrono::milliseconds(20000)));
}
#endif
} // namespace test
} // namespace mediaPlayer
} // namespace alexaClientSDK
int main (int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
if (argc < 2) {
std::cerr << "Usage: MediaPlayerTest <path to test inputs folder>" << std::endl;
} else {
alexaClientSDK::mediaPlayer::test::inputsDirPath = std::string(argv[1]);
return RUN_ALL_TESTS();
}
}