Transcoder: Added PassthroughTrackTranscoder and unit tests.
PassthroughTrackTranscoder is a MediaTrackTranscoder implementation for passthrough mode.
It manages an internal buffer pool to reuse buffers.
This commit also ensures that it is safe to hold on to dequeued MediaSamples
after the MediaTrackTranscoder is released.
Test: Unit test.
Bug: 152091443
Change-Id: I876957647c5fee0557caf465227ff42cdb0eceee
diff --git a/media/libmediatranscoding/transcoder/tests/Android.bp b/media/libmediatranscoding/transcoder/tests/Android.bp
index 48449f8..52a7a71 100644
--- a/media/libmediatranscoding/transcoder/tests/Android.bp
+++ b/media/libmediatranscoding/transcoder/tests/Android.bp
@@ -50,6 +50,7 @@
name: "MediaTrackTranscoderTests",
defaults: ["testdefaults"],
srcs: ["MediaTrackTranscoderTests.cpp"],
+ shared_libs: ["libbinder_ndk"],
}
// VideoTrackTranscoder unit test
@@ -58,3 +59,11 @@
defaults: ["testdefaults"],
srcs: ["VideoTrackTranscoderTests.cpp"],
}
+
+// PassthroughTrackTranscoder unit test
+cc_test {
+ name: "PassthroughTrackTranscoderTests",
+ defaults: ["testdefaults"],
+ srcs: ["PassthroughTrackTranscoderTests.cpp"],
+ shared_libs: ["libcrypto"],
+}
diff --git a/media/libmediatranscoding/transcoder/tests/MediaTrackTranscoderTests.cpp b/media/libmediatranscoding/transcoder/tests/MediaTrackTranscoderTests.cpp
index d1f3fee..c5b181d 100644
--- a/media/libmediatranscoding/transcoder/tests/MediaTrackTranscoderTests.cpp
+++ b/media/libmediatranscoding/transcoder/tests/MediaTrackTranscoderTests.cpp
@@ -20,10 +20,12 @@
#define LOG_TAG "MediaTrackTranscoderTests"
#include <android-base/logging.h>
+#include <android/binder_process.h>
#include <fcntl.h>
#include <gtest/gtest.h>
#include <media/MediaSampleReaderNDK.h>
#include <media/MediaTrackTranscoder.h>
+#include <media/PassthroughTrackTranscoder.h>
#include <media/VideoTrackTranscoder.h>
#include "TrackTranscoderTestUtils.h"
@@ -33,6 +35,7 @@
/** TrackTranscoder types to test. */
enum TrackTranscoderType {
VIDEO,
+ PASSTHROUGH,
};
class MediaTrackTranscoderTests : public ::testing::TestWithParam<TrackTranscoderType> {
@@ -42,12 +45,18 @@
void SetUp() override {
LOG(DEBUG) << "MediaTrackTranscoderTests set up";
+ // Need to start a thread pool to prevent AMediaExtractor binder calls from starving
+ // (b/155663561).
+ ABinderProcess_startThreadPool();
+
mCallback = std::make_shared<TestCallback>();
switch (GetParam()) {
case VIDEO:
mTranscoder = std::make_shared<VideoTrackTranscoder>(mCallback);
- ASSERT_NE(mTranscoder, nullptr);
+ break;
+ case PASSTHROUGH:
+ mTranscoder = std::make_shared<PassthroughTrackTranscoder>(mCallback);
break;
}
ASSERT_NE(mTranscoder, nullptr);
@@ -89,6 +98,14 @@
TrackTranscoderTestUtils::getDefaultVideoDestinationFormat(trackFormat);
ASSERT_NE(mDestinationFormat, nullptr);
break;
+ } else if (GetParam() == PASSTHROUGH && strncmp(mime, "audio/", 6) == 0) {
+ // TODO(lnilsson): Test metadata track passthrough after hkuang@ provides sample.
+ mTrackIndex = trackIndex;
+
+ mSourceFormat = std::shared_ptr<AMediaFormat>(
+ trackFormat, std::bind(AMediaFormat_delete, std::placeholders::_1));
+ ASSERT_NE(mSourceFormat, nullptr);
+ break;
}
AMediaFormat_delete(trackFormat);
@@ -215,7 +232,6 @@
EXPECT_EQ(mCallback->waitUntilFinished(), AMEDIA_OK);
EXPECT_TRUE(mTranscoder->stop());
EXPECT_FALSE(mTranscoder->start());
-
joinDrainThread();
EXPECT_FALSE(mQueueWasAborted);
EXPECT_TRUE(mGotEndOfStream);
@@ -236,11 +252,46 @@
EXPECT_FALSE(mGotEndOfStream);
}
+TEST_P(MediaTrackTranscoderTests, HoldSampleAfterTranscoderRelease) {
+ LOG(DEBUG) << "Testing HoldSampleAfterTranscoderRelease";
+ EXPECT_EQ(mTranscoder->configure(mMediaSampleReader, mTrackIndex, mDestinationFormat),
+ AMEDIA_OK);
+ ASSERT_TRUE(mTranscoder->start());
+
+ std::shared_ptr<MediaSample> sample;
+ EXPECT_FALSE(mTranscoder->mOutputQueue.dequeue(&sample));
+
+ drainOutputSampleQueue();
+ EXPECT_EQ(mCallback->waitUntilFinished(), AMEDIA_OK);
+ EXPECT_TRUE(mTranscoder->stop());
+ joinDrainThread();
+ EXPECT_FALSE(mQueueWasAborted);
+ EXPECT_TRUE(mGotEndOfStream);
+
+ mTranscoder.reset();
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
+ sample.reset();
+}
+
+TEST_P(MediaTrackTranscoderTests, HoldSampleAfterTranscoderStop) {
+ LOG(DEBUG) << "Testing HoldSampleAfterTranscoderStop";
+ EXPECT_EQ(mTranscoder->configure(mMediaSampleReader, mTrackIndex, mDestinationFormat),
+ AMEDIA_OK);
+ ASSERT_TRUE(mTranscoder->start());
+
+ std::shared_ptr<MediaSample> sample;
+ EXPECT_FALSE(mTranscoder->mOutputQueue.dequeue(&sample));
+ EXPECT_TRUE(mTranscoder->stop());
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
+ sample.reset();
+}
+
TEST_P(MediaTrackTranscoderTests, NullSampleReader) {
LOG(DEBUG) << "Testing NullSampleReader";
std::shared_ptr<MediaSampleReader> nullSampleReader;
EXPECT_NE(mTranscoder->configure(nullSampleReader, mTrackIndex, mDestinationFormat), AMEDIA_OK);
- EXPECT_FALSE(mTranscoder->start());
+ ASSERT_FALSE(mTranscoder->start());
}
TEST_P(MediaTrackTranscoderTests, InvalidTrackIndex) {
@@ -256,7 +307,7 @@
using namespace android;
INSTANTIATE_TEST_SUITE_P(MediaTrackTranscoderTestsAll, MediaTrackTranscoderTests,
- ::testing::Values(VIDEO));
+ ::testing::Values(VIDEO, PASSTHROUGH));
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
diff --git a/media/libmediatranscoding/transcoder/tests/PassthroughTrackTranscoderTests.cpp b/media/libmediatranscoding/transcoder/tests/PassthroughTrackTranscoderTests.cpp
new file mode 100644
index 0000000..7a92a37
--- /dev/null
+++ b/media/libmediatranscoding/transcoder/tests/PassthroughTrackTranscoderTests.cpp
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Unit Test for PassthroughTrackTranscoder
+
+// #define LOG_NDEBUG 0
+#define LOG_TAG "PassthroughTrackTranscoderTests"
+
+#include <android-base/logging.h>
+#include <fcntl.h>
+#include <gtest/gtest.h>
+#include <media/MediaSampleReaderNDK.h>
+#include <media/NdkMediaExtractor.h>
+#include <media/PassthroughTrackTranscoder.h>
+#include <openssl/md5.h>
+
+#include <vector>
+
+#include "TrackTranscoderTestUtils.h"
+
+namespace android {
+
+class PassthroughTrackTranscoderTests : public ::testing::Test {
+public:
+ PassthroughTrackTranscoderTests() { LOG(DEBUG) << "PassthroughTrackTranscoderTests created"; }
+
+ void SetUp() override { LOG(DEBUG) << "PassthroughTrackTranscoderTests set up"; }
+
+ void initSourceAndExtractor() {
+ const char* sourcePath =
+ "/data/local/tmp/TranscoderTestAssets/cubicle_avc_480x240_aac_24KHz.mp4";
+
+ mExtractor = AMediaExtractor_new();
+ ASSERT_NE(mExtractor, nullptr);
+
+ mSourceFd = open(sourcePath, O_RDONLY);
+ ASSERT_GT(mSourceFd, 0);
+
+ mSourceFileSize = lseek(mSourceFd, 0, SEEK_END);
+ lseek(mSourceFd, 0, SEEK_SET);
+
+ media_status_t status =
+ AMediaExtractor_setDataSourceFd(mExtractor, mSourceFd, 0, mSourceFileSize);
+ ASSERT_EQ(status, AMEDIA_OK);
+
+ const size_t trackCount = AMediaExtractor_getTrackCount(mExtractor);
+ for (size_t trackIndex = 0; trackIndex < trackCount; trackIndex++) {
+ AMediaFormat* trackFormat = AMediaExtractor_getTrackFormat(mExtractor, trackIndex);
+ ASSERT_NE(trackFormat, nullptr);
+
+ const char* mime = nullptr;
+ AMediaFormat_getString(trackFormat, AMEDIAFORMAT_KEY_MIME, &mime);
+ ASSERT_NE(mime, nullptr);
+
+ if (strncmp(mime, "audio/", 6) == 0) {
+ mTrackIndex = trackIndex;
+ AMediaExtractor_selectTrack(mExtractor, trackIndex);
+ break;
+ }
+
+ AMediaFormat_delete(trackFormat);
+ }
+ }
+
+ void TearDown() override {
+ LOG(DEBUG) << "PassthroughTrackTranscoderTests tear down";
+ if (mExtractor != nullptr) {
+ AMediaExtractor_delete(mExtractor);
+ mExtractor = nullptr;
+ }
+ if (mSourceFd > 0) {
+ close(mSourceFd);
+ mSourceFd = -1;
+ }
+ }
+
+ ~PassthroughTrackTranscoderTests() {
+ LOG(DEBUG) << "PassthroughTrackTranscoderTests destroyed";
+ }
+
+ int mSourceFd = -1;
+ size_t mSourceFileSize;
+ int mTrackIndex;
+ AMediaExtractor* mExtractor = nullptr;
+};
+
+/** Helper class for comparing sample data using checksums. */
+class SampleID {
+public:
+ SampleID(const uint8_t* sampleData, ssize_t sampleSize) : mSize{sampleSize} {
+ MD5_CTX md5Ctx;
+ MD5_Init(&md5Ctx);
+ MD5_Update(&md5Ctx, sampleData, sampleSize);
+ MD5_Final(mChecksum, &md5Ctx);
+ }
+
+ bool operator==(const SampleID& rhs) const {
+ return mSize == rhs.mSize && memcmp(mChecksum, rhs.mChecksum, MD5_DIGEST_LENGTH) == 0;
+ }
+
+ uint8_t mChecksum[MD5_DIGEST_LENGTH];
+ ssize_t mSize;
+};
+
+/**
+ * Tests that the output samples of PassthroughTrackTranscoder are identical to the source samples
+ * and in correct order.
+ */
+TEST_F(PassthroughTrackTranscoderTests, SampleEquality) {
+ LOG(DEBUG) << "Testing SampleEquality";
+
+ ssize_t bufferSize = 1024;
+ auto buffer = std::make_unique<uint8_t[]>(bufferSize);
+
+ initSourceAndExtractor();
+
+ // Loop through all samples of a track and store size and checksums.
+ std::vector<SampleID> sampleChecksums;
+
+ int64_t sampleTime = AMediaExtractor_getSampleTime(mExtractor);
+ while (sampleTime != -1) {
+ if (AMediaExtractor_getSampleTrackIndex(mExtractor) == mTrackIndex) {
+ ssize_t sampleSize = AMediaExtractor_getSampleSize(mExtractor);
+ if (bufferSize < sampleSize) {
+ bufferSize = sampleSize;
+ buffer = std::make_unique<uint8_t[]>(bufferSize);
+ }
+
+ ssize_t bytesRead =
+ AMediaExtractor_readSampleData(mExtractor, buffer.get(), bufferSize);
+ ASSERT_EQ(bytesRead, sampleSize);
+
+ SampleID sampleId{buffer.get(), sampleSize};
+ sampleChecksums.push_back(sampleId);
+ }
+
+ AMediaExtractor_advance(mExtractor);
+ sampleTime = AMediaExtractor_getSampleTime(mExtractor);
+ }
+
+ // Create and start the transcoder.
+ std::shared_ptr<TestCallback> callback = std::make_shared<TestCallback>();
+ PassthroughTrackTranscoder transcoder{callback};
+
+ std::shared_ptr<MediaSampleReader> mediaSampleReader =
+ MediaSampleReaderNDK::createFromFd(mSourceFd, 0, mSourceFileSize);
+ EXPECT_NE(mediaSampleReader, nullptr);
+
+ EXPECT_EQ(transcoder.configure(mediaSampleReader, mTrackIndex, nullptr /* destinationFormat */),
+ AMEDIA_OK);
+ ASSERT_TRUE(transcoder.start());
+
+ // Pull transcoder's output samples and compare against input checksums.
+ uint64_t sampleCount = 0;
+ std::shared_ptr<MediaSample> sample;
+ while (!transcoder.mOutputQueue.dequeue(&sample)) {
+ ASSERT_NE(sample, nullptr);
+
+ if (sample->info.flags & SAMPLE_FLAG_END_OF_STREAM) {
+ break;
+ }
+
+ SampleID sampleId{sample->buffer, static_cast<ssize_t>(sample->info.size)};
+ EXPECT_TRUE(sampleId == sampleChecksums[sampleCount]);
+ ++sampleCount;
+ }
+
+ EXPECT_EQ(sampleCount, sampleChecksums.size());
+ EXPECT_TRUE(transcoder.stop());
+}
+
+/** Class for testing PassthroughTrackTranscoder's built in buffer pool. */
+class BufferPoolTests : public ::testing::Test {
+public:
+ static constexpr int kMaxBuffers = 5;
+
+ void SetUp() override {
+ LOG(DEBUG) << "BufferPoolTests set up";
+ mBufferPool = std::make_shared<PassthroughTrackTranscoder::BufferPool>(kMaxBuffers);
+ }
+
+ void TearDown() override {
+ LOG(DEBUG) << "BufferPoolTests tear down";
+ mBufferPool.reset();
+ }
+
+ std::shared_ptr<PassthroughTrackTranscoder::BufferPool> mBufferPool;
+};
+
+TEST_F(BufferPoolTests, BufferReuse) {
+ LOG(DEBUG) << "Testing BufferReuse";
+
+ uint8_t* buffer1 = mBufferPool->getBufferWithSize(10);
+ EXPECT_NE(buffer1, nullptr);
+
+ uint8_t* buffer2 = mBufferPool->getBufferWithSize(10);
+ EXPECT_NE(buffer2, nullptr);
+ EXPECT_NE(buffer2, buffer1);
+
+ mBufferPool->returnBuffer(buffer1);
+
+ uint8_t* buffer3 = mBufferPool->getBufferWithSize(10);
+ EXPECT_NE(buffer3, nullptr);
+ EXPECT_NE(buffer3, buffer2);
+ EXPECT_EQ(buffer3, buffer1);
+
+ mBufferPool->returnBuffer(buffer2);
+
+ uint8_t* buffer4 = mBufferPool->getBufferWithSize(10);
+ EXPECT_NE(buffer4, nullptr);
+ EXPECT_NE(buffer4, buffer1);
+ EXPECT_EQ(buffer4, buffer2);
+}
+
+TEST_F(BufferPoolTests, SmallestAvailableBuffer) {
+ LOG(DEBUG) << "Testing SmallestAvailableBuffer";
+
+ uint8_t* buffer1 = mBufferPool->getBufferWithSize(10);
+ EXPECT_NE(buffer1, nullptr);
+
+ uint8_t* buffer2 = mBufferPool->getBufferWithSize(15);
+ EXPECT_NE(buffer2, nullptr);
+ EXPECT_NE(buffer2, buffer1);
+
+ uint8_t* buffer3 = mBufferPool->getBufferWithSize(20);
+ EXPECT_NE(buffer3, nullptr);
+ EXPECT_NE(buffer3, buffer1);
+ EXPECT_NE(buffer3, buffer2);
+
+ mBufferPool->returnBuffer(buffer1);
+ mBufferPool->returnBuffer(buffer2);
+ mBufferPool->returnBuffer(buffer3);
+
+ uint8_t* buffer4 = mBufferPool->getBufferWithSize(11);
+ EXPECT_NE(buffer4, nullptr);
+ EXPECT_EQ(buffer4, buffer2);
+
+ uint8_t* buffer5 = mBufferPool->getBufferWithSize(11);
+ EXPECT_NE(buffer5, nullptr);
+ EXPECT_EQ(buffer5, buffer3);
+}
+
+TEST_F(BufferPoolTests, AddAfterAbort) {
+ LOG(DEBUG) << "Testing AddAfterAbort";
+
+ uint8_t* buffer1 = mBufferPool->getBufferWithSize(10);
+ EXPECT_NE(buffer1, nullptr);
+ mBufferPool->returnBuffer(buffer1);
+
+ mBufferPool->abort();
+ uint8_t* buffer2 = mBufferPool->getBufferWithSize(10);
+ EXPECT_EQ(buffer2, nullptr);
+}
+
+TEST_F(BufferPoolTests, MaximumBuffers) {
+ LOG(DEBUG) << "Testing MaximumBuffers";
+
+ static constexpr size_t kBufferBaseSize = 10;
+ std::unordered_map<uint8_t*, size_t> addressSizeMap;
+
+ // Get kMaxBuffers * 2 new buffers with increasing size.
+ // (Note: Once kMaxBuffers have been allocated, the pool will delete old buffers to accommodate
+ // new ones making the deleted buffers free to be reused by the system's heap memory allocator.
+ // So we cannot test that each new pointer is unique here.)
+ for (int i = 0; i < kMaxBuffers * 2; i++) {
+ size_t size = kBufferBaseSize + i;
+ uint8_t* buffer = mBufferPool->getBufferWithSize(size);
+ EXPECT_NE(buffer, nullptr);
+ addressSizeMap[buffer] = size;
+ mBufferPool->returnBuffer(buffer);
+ }
+
+ // Verify that the pool now contains the kMaxBuffers largest buffers allocated above and that
+ // the buffer of matching size is returned.
+ for (int i = kMaxBuffers; i < kMaxBuffers * 2; i++) {
+ size_t size = kBufferBaseSize + i;
+ uint8_t* buffer = mBufferPool->getBufferWithSize(size);
+ EXPECT_NE(buffer, nullptr);
+
+ auto it = addressSizeMap.find(buffer);
+ ASSERT_NE(it, addressSizeMap.end());
+ EXPECT_EQ(it->second, size);
+ mBufferPool->returnBuffer(buffer);
+ }
+}
+
+} // namespace android
+
+int main(int argc, char** argv) {
+ ::testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
diff --git a/media/libmediatranscoding/transcoder/tests/build_and_run_all_unit_tests.sh b/media/libmediatranscoding/transcoder/tests/build_and_run_all_unit_tests.sh
index 3c30e46..61a2252 100755
--- a/media/libmediatranscoding/transcoder/tests/build_and_run_all_unit_tests.sh
+++ b/media/libmediatranscoding/transcoder/tests/build_and_run_all_unit_tests.sh
@@ -33,3 +33,6 @@
echo "testing VideoTrackTranscoder"
adb shell /data/nativetest64/VideoTrackTranscoderTests/VideoTrackTranscoderTests
+
+echo "testing PassthroughTrackTranscoder"
+adb shell /data/nativetest64/PassthroughTrackTranscoderTests/PassthroughTrackTranscoderTests