/* * Copyright (C) 2019 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. */ //#define LOG_NDEBUG 0 #define LOG_TAG "ExtractorUnitTest" #include #include #include #include #include #include "aac/AACExtractor.h" #include "amr/AMRExtractor.h" #include "flac/FLACExtractor.h" #include "midi/MidiExtractor.h" #include "mkv/MatroskaExtractor.h" #include "mp3/MP3Extractor.h" #include "mp4/MPEG4Extractor.h" #include "mp4/SampleTable.h" #include "mpeg2/MPEG2PSExtractor.h" #include "mpeg2/MPEG2TSExtractor.h" #include "ogg/OggExtractor.h" #include "wav/WAVExtractor.h" #include "ExtractorUnitTestEnvironment.h" using namespace android; #define OUTPUT_DUMP_FILE "/data/local/tmp/extractorOutput" constexpr int32_t kMaxCount = 10; constexpr int32_t kOpusSeekPreRollUs = 80000; // 80 ms; static ExtractorUnitTestEnvironment *gEnv = nullptr; class ExtractorUnitTest : public ::testing::TestWithParam> { public: ExtractorUnitTest() : mInputFp(nullptr), mDataSource(nullptr), mExtractor(nullptr) {} ~ExtractorUnitTest() { if (mInputFp) { fclose(mInputFp); mInputFp = nullptr; } if (mDataSource) { mDataSource.clear(); mDataSource = nullptr; } if (mExtractor) { delete mExtractor; mExtractor = nullptr; } } virtual void SetUp() override { mExtractorName = unknown_comp; mDisableTest = false; static const std::map mapExtractor = { {"aac", AAC}, {"amr", AMR}, {"mp3", MP3}, {"ogg", OGG}, {"wav", WAV}, {"mkv", MKV}, {"flac", FLAC}, {"midi", MIDI}, {"mpeg4", MPEG4}, {"mpeg2ts", MPEG2TS}, {"mpeg2ps", MPEG2PS}}; // Find the component type string writerFormat = GetParam().first; if (mapExtractor.find(writerFormat) != mapExtractor.end()) { mExtractorName = mapExtractor.at(writerFormat); } if (mExtractorName == standardExtractors::unknown_comp) { cout << "[ WARN ] Test Skipped. Invalid extractor\n"; mDisableTest = true; } } int32_t setDataSource(string inputFileName); int32_t createExtractor(); enum standardExtractors { AAC, AMR, FLAC, MIDI, MKV, MP3, MPEG4, MPEG2PS, MPEG2TS, OGG, WAV, unknown_comp, }; bool mDisableTest; standardExtractors mExtractorName; FILE *mInputFp; sp mDataSource; MediaExtractorPluginHelper *mExtractor; }; int32_t ExtractorUnitTest::setDataSource(string inputFileName) { mInputFp = fopen(inputFileName.c_str(), "rb"); if (!mInputFp) { ALOGE("Unable to open input file for reading"); return -1; } struct stat buf; stat(inputFileName.c_str(), &buf); int32_t fd = fileno(mInputFp); mDataSource = new FileSource(dup(fd), 0, buf.st_size); if (!mDataSource) return -1; return 0; } int32_t ExtractorUnitTest::createExtractor() { switch (mExtractorName) { case AAC: mExtractor = new AACExtractor(new DataSourceHelper(mDataSource->wrap()), 0); break; case AMR: mExtractor = new AMRExtractor(new DataSourceHelper(mDataSource->wrap())); break; case MP3: mExtractor = new MP3Extractor(new DataSourceHelper(mDataSource->wrap()), nullptr); break; case OGG: mExtractor = new OggExtractor(new DataSourceHelper(mDataSource->wrap())); break; case WAV: mExtractor = new WAVExtractor(new DataSourceHelper(mDataSource->wrap())); break; case MKV: mExtractor = new MatroskaExtractor(new DataSourceHelper(mDataSource->wrap())); break; case FLAC: mExtractor = new FLACExtractor(new DataSourceHelper(mDataSource->wrap())); break; case MPEG4: mExtractor = new MPEG4Extractor(new DataSourceHelper(mDataSource->wrap())); break; case MPEG2TS: mExtractor = new MPEG2TSExtractor(new DataSourceHelper(mDataSource->wrap())); break; case MPEG2PS: mExtractor = new MPEG2PSExtractor(new DataSourceHelper(mDataSource->wrap())); break; case MIDI: mExtractor = new MidiExtractor(mDataSource->wrap()); break; default: return -1; } if (!mExtractor) return -1; return 0; } void getSeekablePoints(vector &seekablePoints, MediaTrackHelper *track) { int32_t status = 0; if (!seekablePoints.empty()) { seekablePoints.clear(); } int64_t timeStamp; while (status != AMEDIA_ERROR_END_OF_STREAM) { MediaBufferHelper *buffer = nullptr; status = track->read(&buffer); if (buffer) { AMediaFormat *metaData = buffer->meta_data(); int32_t isSync = 0; AMediaFormat_getInt32(metaData, AMEDIAFORMAT_KEY_IS_SYNC_FRAME, &isSync); if (isSync) { AMediaFormat_getInt64(metaData, AMEDIAFORMAT_KEY_TIME_US, &timeStamp); seekablePoints.push_back(timeStamp); } buffer->release(); } } } TEST_P(ExtractorUnitTest, CreateExtractorTest) { if (mDisableTest) return; ALOGV("Checks if a valid extractor is created for a given input file"); string inputFileName = gEnv->getRes() + GetParam().second; ASSERT_EQ(setDataSource(inputFileName), 0) << "SetDataSource failed for" << GetParam().first << "extractor"; ASSERT_EQ(createExtractor(), 0) << "Extractor creation failed for" << GetParam().first << "extractor"; // A valid extractor instace should return success for following calls ASSERT_GT(mExtractor->countTracks(), 0); AMediaFormat *format = AMediaFormat_new(); ASSERT_NE(format, nullptr) << "AMediaFormat_new returned null AMediaformat"; ASSERT_EQ(mExtractor->getMetaData(format), AMEDIA_OK); AMediaFormat_delete(format); } TEST_P(ExtractorUnitTest, ExtractorTest) { if (mDisableTest) return; ALOGV("Validates %s Extractor for a given input file", GetParam().first.c_str()); string inputFileName = gEnv->getRes() + GetParam().second; int32_t status = setDataSource(inputFileName); ASSERT_EQ(status, 0) << "SetDataSource failed for" << GetParam().first << "extractor"; status = createExtractor(); ASSERT_EQ(status, 0) << "Extractor creation failed for" << GetParam().first << "extractor"; int32_t numTracks = mExtractor->countTracks(); ASSERT_GT(numTracks, 0) << "Extractor didn't find any track for the given clip"; for (int32_t idx = 0; idx < numTracks; idx++) { MediaTrackHelper *track = mExtractor->getTrack(idx); ASSERT_NE(track, nullptr) << "Failed to get track for index " << idx; CMediaTrack *cTrack = wrap(track); ASSERT_NE(cTrack, nullptr) << "Failed to get track wrapper for index " << idx; MediaBufferGroup *bufferGroup = new MediaBufferGroup(); status = cTrack->start(track, bufferGroup->wrap()); ASSERT_EQ(OK, (media_status_t)status) << "Failed to start the track"; FILE *outFp = fopen((OUTPUT_DUMP_FILE + to_string(idx)).c_str(), "wb"); if (!outFp) { ALOGW("Unable to open output file for dumping extracted stream"); } while (status != AMEDIA_ERROR_END_OF_STREAM) { MediaBufferHelper *buffer = nullptr; status = track->read(&buffer); ALOGV("track->read Status = %d buffer %p", status, buffer); if (buffer) { ALOGV("buffer->data %p buffer->size() %zu buffer->range_length() %zu", buffer->data(), buffer->size(), buffer->range_length()); if (outFp) fwrite(buffer->data(), 1, buffer->range_length(), outFp); buffer->release(); } } if (outFp) fclose(outFp); status = cTrack->stop(track); ASSERT_EQ(OK, status) << "Failed to stop the track"; delete bufferGroup; delete track; } } TEST_P(ExtractorUnitTest, MetaDataComparisonTest) { if (mDisableTest) return; ALOGV("Validates Extractor's meta data for a given input file"); string inputFileName = gEnv->getRes() + GetParam().second; int32_t status = setDataSource(inputFileName); ASSERT_EQ(status, 0) << "SetDataSource failed for" << GetParam().first << "extractor"; status = createExtractor(); ASSERT_EQ(status, 0) << "Extractor creation failed for" << GetParam().first << "extractor"; int32_t numTracks = mExtractor->countTracks(); ASSERT_GT(numTracks, 0) << "Extractor didn't find any track for the given clip"; AMediaFormat *extractorFormat = AMediaFormat_new(); ASSERT_NE(extractorFormat, nullptr) << "AMediaFormat_new returned null AMediaformat"; AMediaFormat *trackFormat = AMediaFormat_new(); ASSERT_NE(trackFormat, nullptr) << "AMediaFormat_new returned null AMediaformat"; for (int32_t idx = 0; idx < numTracks; idx++) { MediaTrackHelper *track = mExtractor->getTrack(idx); ASSERT_NE(track, nullptr) << "Failed to get track for index " << idx; CMediaTrack *cTrack = wrap(track); ASSERT_NE(cTrack, nullptr) << "Failed to get track wrapper for index " << idx; MediaBufferGroup *bufferGroup = new MediaBufferGroup(); status = cTrack->start(track, bufferGroup->wrap()); ASSERT_EQ(OK, (media_status_t)status) << "Failed to start the track"; status = mExtractor->getTrackMetaData(extractorFormat, idx, 1); ASSERT_EQ(OK, (media_status_t)status) << "Failed to get trackMetaData"; status = track->getFormat(trackFormat); ASSERT_EQ(OK, (media_status_t)status) << "Failed to get track meta data"; const char *extractorMime, *trackMime; AMediaFormat_getString(extractorFormat, AMEDIAFORMAT_KEY_MIME, &extractorMime); AMediaFormat_getString(trackFormat, AMEDIAFORMAT_KEY_MIME, &trackMime); ASSERT_TRUE(!strcmp(extractorMime, trackMime)) << "Extractor's format doesn't match track format"; if (!strncmp(extractorMime, "audio/", 6)) { int32_t exSampleRate, exChannelCount; int32_t trackSampleRate, trackChannelCount; ASSERT_TRUE(AMediaFormat_getInt32(extractorFormat, AMEDIAFORMAT_KEY_CHANNEL_COUNT, &exChannelCount)); ASSERT_TRUE(AMediaFormat_getInt32(extractorFormat, AMEDIAFORMAT_KEY_SAMPLE_RATE, &exSampleRate)); ASSERT_TRUE(AMediaFormat_getInt32(trackFormat, AMEDIAFORMAT_KEY_CHANNEL_COUNT, &trackChannelCount)); ASSERT_TRUE(AMediaFormat_getInt32(trackFormat, AMEDIAFORMAT_KEY_SAMPLE_RATE, &trackSampleRate)); ASSERT_EQ(exChannelCount, trackChannelCount) << "ChannelCount not as expected"; ASSERT_EQ(exSampleRate, trackSampleRate) << "SampleRate not as expected"; } else { int32_t exWidth, exHeight; int32_t trackWidth, trackHeight; ASSERT_TRUE(AMediaFormat_getInt32(extractorFormat, AMEDIAFORMAT_KEY_WIDTH, &exWidth)); ASSERT_TRUE(AMediaFormat_getInt32(extractorFormat, AMEDIAFORMAT_KEY_HEIGHT, &exHeight)); ASSERT_TRUE(AMediaFormat_getInt32(trackFormat, AMEDIAFORMAT_KEY_WIDTH, &trackWidth)); ASSERT_TRUE(AMediaFormat_getInt32(trackFormat, AMEDIAFORMAT_KEY_HEIGHT, &trackHeight)); ASSERT_EQ(exWidth, trackWidth) << "Width not as expected"; ASSERT_EQ(exHeight, trackHeight) << "Height not as expected"; } status = cTrack->stop(track); ASSERT_EQ(OK, status) << "Failed to stop the track"; delete bufferGroup; delete track; } AMediaFormat_delete(trackFormat); AMediaFormat_delete(extractorFormat); } TEST_P(ExtractorUnitTest, MultipleStartStopTest) { if (mDisableTest) return; ALOGV("Test %s extractor for multiple start and stop calls", GetParam().first.c_str()); string inputFileName = gEnv->getRes() + GetParam().second; int32_t status = setDataSource(inputFileName); ASSERT_EQ(status, 0) << "SetDataSource failed for" << GetParam().first << "extractor"; status = createExtractor(); ASSERT_EQ(status, 0) << "Extractor creation failed for" << GetParam().first << "extractor"; int32_t numTracks = mExtractor->countTracks(); ASSERT_GT(numTracks, 0) << "Extractor didn't find any track for the given clip"; // start/stop the tracks multiple times for (int32_t count = 0; count < kMaxCount; count++) { for (int32_t idx = 0; idx < numTracks; idx++) { MediaTrackHelper *track = mExtractor->getTrack(idx); ASSERT_NE(track, nullptr) << "Failed to get track for index " << idx; CMediaTrack *cTrack = wrap(track); ASSERT_NE(cTrack, nullptr) << "Failed to get track wrapper for index " << idx; MediaBufferGroup *bufferGroup = new MediaBufferGroup(); status = cTrack->start(track, bufferGroup->wrap()); ASSERT_EQ(OK, (media_status_t)status) << "Failed to start the track"; MediaBufferHelper *buffer = nullptr; status = track->read(&buffer); if (buffer) { ALOGV("buffer->data %p buffer->size() %zu buffer->range_length() %zu", buffer->data(), buffer->size(), buffer->range_length()); buffer->release(); } status = cTrack->stop(track); ASSERT_EQ(OK, status) << "Failed to stop the track"; delete bufferGroup; delete track; } } } TEST_P(ExtractorUnitTest, SeekTest) { // Both Flac and Wav extractor can give samples from any pts and mark the given sample as // sync frame. So, this seek test is not applicable to FLAC and WAV extractors if (mDisableTest || mExtractorName == FLAC || mExtractorName == WAV) return; ALOGV("Validates %s Extractor behaviour for different seek modes", GetParam().first.c_str()); string inputFileName = gEnv->getRes() + GetParam().second; int32_t status = setDataSource(inputFileName); ASSERT_EQ(status, 0) << "SetDataSource failed for" << GetParam().first << "extractor"; status = createExtractor(); ASSERT_EQ(status, 0) << "Extractor creation failed for" << GetParam().first << "extractor"; int32_t numTracks = mExtractor->countTracks(); ASSERT_GT(numTracks, 0) << "Extractor didn't find any track for the given clip"; uint32_t seekFlag = mExtractor->flags(); if (!(seekFlag & MediaExtractorPluginHelper::CAN_SEEK)) { cout << "[ WARN ] Test Skipped. " << GetParam().first << " Extractor doesn't support seek\n"; return; } vector seekablePoints; for (int32_t idx = 0; idx < numTracks; idx++) { MediaTrackHelper *track = mExtractor->getTrack(idx); ASSERT_NE(track, nullptr) << "Failed to get track for index " << idx; CMediaTrack *cTrack = wrap(track); ASSERT_NE(cTrack, nullptr) << "Failed to get track wrapper for index " << idx; // Get all the seekable points of a given input MediaBufferGroup *bufferGroup = new MediaBufferGroup(); status = cTrack->start(track, bufferGroup->wrap()); ASSERT_EQ(OK, (media_status_t)status) << "Failed to start the track"; getSeekablePoints(seekablePoints, track); ASSERT_GT(seekablePoints.size(), 0) << "Failed to get seekable points for " << GetParam().first << " extractor"; AMediaFormat *trackFormat = AMediaFormat_new(); ASSERT_NE(trackFormat, nullptr) << "AMediaFormat_new returned null format"; status = track->getFormat(trackFormat); ASSERT_EQ(OK, (media_status_t)status) << "Failed to get track meta data"; bool isOpus = false; const char *mime; AMediaFormat_getString(trackFormat, AMEDIAFORMAT_KEY_MIME, &mime); if (!strcmp(mime, "audio/opus")) isOpus = true; AMediaFormat_delete(trackFormat); int32_t seekIdx = 0; size_t seekablePointsSize = seekablePoints.size(); for (int32_t mode = CMediaTrackReadOptions::SEEK_PREVIOUS_SYNC; mode <= CMediaTrackReadOptions::SEEK_CLOSEST; mode++) { for (int32_t seekCount = 0; seekCount < kMaxCount; seekCount++) { seekIdx = rand() % seekablePointsSize + 1; if (seekIdx >= seekablePointsSize) seekIdx = seekablePointsSize - 1; int64_t seekToTimeStamp = seekablePoints[seekIdx]; if (seekablePointsSize > 1) { int64_t prevTimeStamp = seekablePoints[seekIdx - 1]; seekToTimeStamp = seekToTimeStamp - ((seekToTimeStamp - prevTimeStamp) >> 3); } // Opus has a seekPreRollUs. TimeStamp returned by the // extractor is calculated based on (seekPts - seekPreRollUs). // So we add the preRoll value to the timeStamp we want to seek to. if (isOpus) { seekToTimeStamp += kOpusSeekPreRollUs; } MediaTrackHelper::ReadOptions *options = new MediaTrackHelper::ReadOptions( mode | CMediaTrackReadOptions::SEEK, seekToTimeStamp); ASSERT_NE(options, nullptr) << "Cannot create read option"; MediaBufferHelper *buffer = nullptr; status = track->read(&buffer, options); if (status == AMEDIA_ERROR_END_OF_STREAM) { delete options; continue; } if (buffer) { AMediaFormat *metaData = buffer->meta_data(); int64_t timeStamp; AMediaFormat_getInt64(metaData, AMEDIAFORMAT_KEY_TIME_US, &timeStamp); buffer->release(); // CMediaTrackReadOptions::SEEK is 8. Using mask 0111b to get true modes switch (mode & 0x7) { case CMediaTrackReadOptions::SEEK_PREVIOUS_SYNC: if (seekablePointsSize == 1) { EXPECT_EQ(timeStamp, seekablePoints[seekIdx]); } else { EXPECT_EQ(timeStamp, seekablePoints[seekIdx - 1]); } break; case CMediaTrackReadOptions::SEEK_NEXT_SYNC: case CMediaTrackReadOptions::SEEK_CLOSEST_SYNC: case CMediaTrackReadOptions::SEEK_CLOSEST: EXPECT_EQ(timeStamp, seekablePoints[seekIdx]); break; default: break; } } delete options; } } status = cTrack->stop(track); ASSERT_EQ(OK, status) << "Failed to stop the track"; delete bufferGroup; delete track; } seekablePoints.clear(); } // TODO: (b/145332185) // Add MIDI inputs INSTANTIATE_TEST_SUITE_P(ExtractorUnitTestAll, ExtractorUnitTest, ::testing::Values(make_pair("aac", "loudsoftaac.aac"), make_pair("amr", "testamr.amr"), make_pair("amr", "amrwb.wav"), make_pair("ogg", "john_cage.ogg"), make_pair("wav", "monotestgsm.wav"), make_pair("mpeg2ts", "segment000001.ts"), make_pair("flac", "sinesweepflac.flac"), make_pair("ogg", "testopus.opus"), make_pair("mkv", "sinesweepvorbis.mkv"), make_pair("mpeg4", "sinesweepoggmp4.mp4"), make_pair("mp3", "sinesweepmp3lame.mp3"), make_pair("mkv", "swirl_144x136_vp9.webm"), make_pair("mkv", "swirl_144x136_vp8.webm"), make_pair("mpeg2ps", "swirl_144x136_mpeg2.mpg"), make_pair("mpeg4", "swirl_132x130_mpeg4.mp4"))); int main(int argc, char **argv) { gEnv = new ExtractorUnitTestEnvironment(); ::testing::AddGlobalTestEnvironment(gEnv); ::testing::InitGoogleTest(&argc, argv); int status = gEnv->initFromOptions(argc, argv); if (status == 0) { status = RUN_ALL_TESTS(); ALOGV("Test result = %d\n", status); } return status; }