| /* |
| * 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 "NativeMuxerTest" |
| #include <log/log.h> |
| |
| #include <NdkMediaExtractor.h> |
| #include <NdkMediaFormat.h> |
| #include <NdkMediaMuxer.h> |
| #include <fcntl.h> |
| #include <jni.h> |
| #include <sys/stat.h> |
| #include <unistd.h> |
| |
| #include <cmath> |
| #include <cstring> |
| #include <fstream> |
| #include <map> |
| #include <vector> |
| |
| #include "NativeMediaConstants.h" |
| |
| /** |
| * MuxerNativeTestHelper breaks a media file to elements that a muxer can use to rebuild its clone. |
| * While testing muxer, if the test doesn't use MediaCodecs class to generate elementary stream, |
| * but uses MediaExtractor, this class will be handy |
| */ |
| class MuxerNativeTestHelper { |
| public: |
| explicit MuxerNativeTestHelper(const char* srcPath, const char* mime = nullptr, |
| int frameLimit = -1) |
| : mSrcPath(srcPath), mMime(mime), mTrackCount(0), mBuffer(nullptr) { |
| mFrameLimit = frameLimit < 0 ? INT_MAX : frameLimit; |
| splitMediaToMuxerParameters(); |
| } |
| |
| ~MuxerNativeTestHelper() { |
| for (auto format : mFormat) AMediaFormat_delete(format); |
| delete[] mBuffer; |
| for (const auto& buffInfoTrack : mBufferInfo) { |
| for (auto info : buffInfoTrack) delete info; |
| } |
| } |
| |
| int getTrackCount() { return mTrackCount; } |
| |
| bool registerTrack(AMediaMuxer* muxer); |
| |
| bool insertSampleData(AMediaMuxer* muxer); |
| |
| bool muxMedia(AMediaMuxer* muxer); |
| |
| bool combineMedias(AMediaMuxer* muxer, MuxerNativeTestHelper* that, const int* repeater); |
| |
| bool equals(MuxerNativeTestHelper* that); |
| |
| void offsetTimeStamp(int trackID, long tsOffset, int sampleOffset); |
| |
| private: |
| void splitMediaToMuxerParameters(); |
| |
| static const int STTS_TOLERANCE = 100; |
| const char* mSrcPath; |
| const char* mMime; |
| int mTrackCount; |
| std::vector<AMediaFormat*> mFormat; |
| uint8_t* mBuffer; |
| std::vector<std::vector<AMediaCodecBufferInfo*>> mBufferInfo; |
| std::map<int, int> mInpIndexMap; |
| std::vector<int> mTrackIdxOrder; |
| int mFrameLimit; |
| // combineMedias() uses local version of this variable |
| std::map<int, int> mOutIndexMap; |
| }; |
| |
| void MuxerNativeTestHelper::splitMediaToMuxerParameters() { |
| FILE* ifp = fopen(mSrcPath, "rbe"); |
| int fd; |
| int fileSize; |
| if (ifp) { |
| struct stat buf {}; |
| stat(mSrcPath, &buf); |
| fileSize = buf.st_size; |
| fd = fileno(ifp); |
| } else { |
| return; |
| } |
| AMediaExtractor* extractor = AMediaExtractor_new(); |
| if (extractor == nullptr) { |
| fclose(ifp); |
| return; |
| } |
| // Set up MediaExtractor to read from the source. |
| media_status_t status = AMediaExtractor_setDataSourceFd(extractor, fd, 0, fileSize); |
| if (AMEDIA_OK != status) { |
| AMediaExtractor_delete(extractor); |
| fclose(ifp); |
| return; |
| } |
| |
| // Set up MediaFormat |
| int index = 0; |
| for (size_t trackID = 0; trackID < AMediaExtractor_getTrackCount(extractor); trackID++) { |
| AMediaExtractor_selectTrack(extractor, trackID); |
| AMediaFormat* format = AMediaExtractor_getTrackFormat(extractor, trackID); |
| if (mMime == nullptr) { |
| mTrackCount++; |
| mFormat.push_back(format); |
| mInpIndexMap[trackID] = index++; |
| } else { |
| const char* mime; |
| bool hasKey = AMediaFormat_getString(format, AMEDIAFORMAT_KEY_MIME, &mime); |
| if (hasKey && !strcmp(mime, mMime)) { |
| mTrackCount++; |
| mFormat.push_back(format); |
| mInpIndexMap[trackID] = index; |
| break; |
| } else { |
| AMediaFormat_delete(format); |
| AMediaExtractor_unselectTrack(extractor, trackID); |
| } |
| } |
| } |
| |
| if (mTrackCount <= 0) { |
| AMediaExtractor_delete(extractor); |
| fclose(ifp); |
| return; |
| } |
| |
| // Set up location for elementary stream |
| int bufferSize = ((fileSize + 127) >> 7) << 7; |
| // Ideally, Sum of return values of extractor.readSampleData(...) should not exceed |
| // source file size. But in case of Vorbis, aosp extractor appends an additional 4 bytes to |
| // the data at every readSampleData() call. bufferSize <<= 1 empirically large enough to |
| // hold the excess 4 bytes per read call |
| bufferSize <<= 1; |
| mBuffer = new uint8_t[bufferSize]; |
| if (mBuffer == nullptr) { |
| mTrackCount = 0; |
| AMediaExtractor_delete(extractor); |
| fclose(ifp); |
| return; |
| } |
| |
| // Let MediaExtractor do its thing |
| bool sawEOS = false; |
| int frameCount = 0; |
| int offset = 0; |
| mBufferInfo.resize(mTrackCount); |
| while (!sawEOS && frameCount < mFrameLimit) { |
| auto* bufferInfo = new AMediaCodecBufferInfo(); |
| bufferInfo->offset = offset; |
| bufferInfo->size = |
| AMediaExtractor_readSampleData(extractor, mBuffer + offset, (bufferSize - offset)); |
| if (bufferInfo->size < 0) { |
| sawEOS = true; |
| } else { |
| bufferInfo->presentationTimeUs = AMediaExtractor_getSampleTime(extractor); |
| bufferInfo->flags = AMediaExtractor_getSampleFlags(extractor); |
| int trackID = AMediaExtractor_getSampleTrackIndex(extractor); |
| mTrackIdxOrder.push_back(trackID); |
| mBufferInfo[(mInpIndexMap.at(trackID))].push_back(bufferInfo); |
| AMediaExtractor_advance(extractor); |
| frameCount++; |
| } |
| offset += bufferInfo->size; |
| } |
| |
| AMediaExtractor_delete(extractor); |
| fclose(ifp); |
| } |
| |
| bool MuxerNativeTestHelper::registerTrack(AMediaMuxer* muxer) { |
| for (int trackID = 0; trackID < mTrackCount; trackID++) { |
| int dstIndex = AMediaMuxer_addTrack(muxer, mFormat[trackID]); |
| if (dstIndex < 0) return false; |
| mOutIndexMap[trackID] = dstIndex; |
| } |
| return true; |
| } |
| |
| bool MuxerNativeTestHelper::insertSampleData(AMediaMuxer* muxer) { |
| // write all registered tracks in interleaved order |
| int* frameCount = new int[mTrackCount]{0}; |
| for (int trackID : mTrackIdxOrder) { |
| int index = mInpIndexMap.at(trackID); |
| AMediaCodecBufferInfo* info = mBufferInfo[index][frameCount[index]]; |
| if (AMediaMuxer_writeSampleData(muxer, mOutIndexMap.at(index), mBuffer, info) != |
| AMEDIA_OK) { |
| delete[] frameCount; |
| return false; |
| } |
| ALOGV("Track: %d Timestamp: %d", trackID, (int)info->presentationTimeUs); |
| frameCount[index]++; |
| } |
| delete[] frameCount; |
| ALOGV("Total track samples %d", (int)mTrackIdxOrder.size()); |
| return true; |
| } |
| |
| bool MuxerNativeTestHelper::muxMedia(AMediaMuxer* muxer) { |
| return (registerTrack(muxer) && (AMediaMuxer_start(muxer) == AMEDIA_OK) && |
| insertSampleData(muxer) && (AMediaMuxer_stop(muxer) == AMEDIA_OK)); |
| } |
| |
| bool MuxerNativeTestHelper::combineMedias(AMediaMuxer* muxer, MuxerNativeTestHelper* that, |
| const int* repeater) { |
| if (that == nullptr) return false; |
| if (repeater == nullptr) return false; |
| |
| // register tracks |
| int totalTracksToAdd = repeater[0] * this->mTrackCount + repeater[1] * that->mTrackCount; |
| int outIndexMap[totalTracksToAdd]; |
| MuxerNativeTestHelper* group[2]{this, that}; |
| for (int k = 0, idx = 0; k < 2; k++) { |
| for (int j = 0; j < repeater[k]; j++) { |
| for (AMediaFormat* format : group[k]->mFormat) { |
| int dstIndex = AMediaMuxer_addTrack(muxer, format); |
| if (dstIndex < 0) return false; |
| outIndexMap[idx++] = dstIndex; |
| } |
| } |
| } |
| // start |
| if (AMediaMuxer_start(muxer) != AMEDIA_OK) return false; |
| // write sample data |
| // write all registered tracks in planar order viz all samples of a track A then all |
| // samples of track B, ... |
| for (int k = 0, idx = 0; k < 2; k++) { |
| for (int j = 0; j < repeater[k]; j++) { |
| for (int i = 0; i < group[k]->mTrackCount; i++) { |
| for (int p = 0; p < group[k]->mBufferInfo[i].size(); p++) { |
| AMediaCodecBufferInfo* info = group[k]->mBufferInfo[i][p]; |
| if (AMediaMuxer_writeSampleData(muxer, outIndexMap[idx], group[k]->mBuffer, |
| info) != AMEDIA_OK) { |
| return false; |
| } |
| ALOGV("Track: %d Timestamp: %d", outIndexMap[idx], |
| (int)info->presentationTimeUs); |
| } |
| idx++; |
| } |
| } |
| } |
| // stop |
| return (AMediaMuxer_stop(muxer) == AMEDIA_OK); |
| } |
| |
| // returns true if 'this' stream is a subset of 'o'. That is all tracks in current media |
| // stream are present in ref media stream |
| bool MuxerNativeTestHelper::equals(MuxerNativeTestHelper* that) { |
| if (this == that) return true; |
| if (that == nullptr) return false; |
| |
| for (int i = 0; i < mTrackCount; i++) { |
| AMediaFormat* thisFormat = mFormat[i]; |
| const char* thisMime = nullptr; |
| AMediaFormat_getString(thisFormat, AMEDIAFORMAT_KEY_MIME, &thisMime); |
| int j = 0; |
| for (; j < that->mTrackCount; j++) { |
| AMediaFormat* thatFormat = that->mFormat[j]; |
| const char* thatMime = nullptr; |
| AMediaFormat_getString(thatFormat, AMEDIAFORMAT_KEY_MIME, &thatMime); |
| if (thisMime != nullptr && thatMime != nullptr && !strcmp(thisMime, thatMime)) { |
| if (mBufferInfo[i].size() == that->mBufferInfo[j].size()) { |
| int flagsDiff = 0, sizeDiff = 0, tsDiff = 0; |
| for (int k = 0; k < mBufferInfo[i].size(); k++) { |
| AMediaCodecBufferInfo* thisInfo = mBufferInfo[i][k]; |
| AMediaCodecBufferInfo* thatInfo = that->mBufferInfo[j][k]; |
| if (thisInfo->flags != thatInfo->flags) { |
| flagsDiff++; |
| } |
| if (thisInfo->size != thatInfo->size) { |
| sizeDiff++; |
| } |
| if (abs(thisInfo->presentationTimeUs - thatInfo->presentationTimeUs) > |
| STTS_TOLERANCE) { |
| tsDiff++; |
| } |
| } |
| if (flagsDiff == 0 && sizeDiff == 0 && tsDiff == 0) |
| break; |
| else { |
| ALOGV("For mime %s, Total Samples %d, flagsDiff %d, sizeDiff %d, tsDiff %d", |
| thisMime, (int)mBufferInfo[i].size(), flagsDiff, sizeDiff, tsDiff); |
| } |
| } |
| } |
| } |
| if (j == that->mTrackCount) { |
| ALOGV("For mime %s, Couldn't find a match", thisMime); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| void MuxerNativeTestHelper::offsetTimeStamp(int trackID, long tsOffset, int sampleOffset) { |
| // offset pts of samples from index sampleOffset till the end by tsOffset |
| if (trackID < mTrackCount) { |
| for (int i = sampleOffset; i < mBufferInfo[trackID].size(); i++) { |
| AMediaCodecBufferInfo* info = mBufferInfo[trackID][i]; |
| info->presentationTimeUs += tsOffset; |
| } |
| } |
| } |
| |
| static bool isCodecContainerPairValid(MuxerFormat format, const char* mime) { |
| static const std::map<MuxerFormat, std::vector<const char*>> codecListforType = { |
| {OUTPUT_FORMAT_MPEG_4, |
| {AMEDIA_MIMETYPE_VIDEO_MPEG4, AMEDIA_MIMETYPE_VIDEO_H263, AMEDIA_MIMETYPE_VIDEO_AVC, |
| AMEDIA_MIMETYPE_VIDEO_HEVC, AMEDIA_MIMETYPE_AUDIO_AAC}}, |
| {OUTPUT_FORMAT_WEBM, |
| {AMEDIA_MIMETYPE_VIDEO_VP8, AMEDIA_MIMETYPE_VIDEO_VP9, AMEDIA_MIMETYPE_AUDIO_VORBIS, |
| AMEDIA_MIMETYPE_AUDIO_OPUS}}, |
| {OUTPUT_FORMAT_THREE_GPP, |
| {AMEDIA_MIMETYPE_VIDEO_MPEG4, AMEDIA_MIMETYPE_VIDEO_H263, AMEDIA_MIMETYPE_VIDEO_AVC, |
| AMEDIA_MIMETYPE_AUDIO_AAC, AMEDIA_MIMETYPE_AUDIO_AMR_NB, |
| AMEDIA_MIMETYPE_AUDIO_AMR_WB}}, |
| {OUTPUT_FORMAT_OGG, {AMEDIA_MIMETYPE_AUDIO_OPUS}}, |
| }; |
| |
| if (format == OUTPUT_FORMAT_MPEG_4 && |
| strncmp(mime, "application/", strlen("application/")) == 0) |
| return true; |
| |
| auto it = codecListforType.find(format); |
| if (it != codecListforType.end()) |
| for (auto it2 : it->second) |
| if (strcmp(it2, mime) == 0) return true; |
| |
| return false; |
| } |
| |
| static jboolean nativeTestSetLocation(JNIEnv* env, jobject, jint jformat, jstring jsrcPath, |
| jstring jdstPath) { |
| bool isPass = true; |
| bool isGeoDataSupported; |
| const float atlanticLat = 14.59f; |
| const float atlanticLong = 28.67f; |
| const float tooFarNorth = 90.5f; |
| const float tooFarWest = -180.5f; |
| const float tooFarSouth = -90.5f; |
| const float tooFarEast = 180.5f; |
| const float annapurnaLat = 28.59f; |
| const float annapurnaLong = 83.82f; |
| const char* cdstPath = env->GetStringUTFChars(jdstPath, nullptr); |
| FILE* ofp = fopen(cdstPath, "wbe+"); |
| if (ofp) { |
| AMediaMuxer* muxer = AMediaMuxer_new(fileno(ofp), (OutputFormat)jformat); |
| media_status_t status = AMediaMuxer_setLocation(muxer, tooFarNorth, atlanticLong); |
| if (status == AMEDIA_OK) { |
| isPass = false; |
| ALOGE("setLocation succeeds on bad args: (%f, %f)", tooFarNorth, atlanticLong); |
| } |
| status = AMediaMuxer_setLocation(muxer, tooFarSouth, atlanticLong); |
| if (status == AMEDIA_OK) { |
| isPass = false; |
| ALOGE("setLocation succeeds on bad args: (%f, %f)", tooFarSouth, atlanticLong); |
| } |
| status = AMediaMuxer_setLocation(muxer, atlanticLat, tooFarWest); |
| if (status == AMEDIA_OK) { |
| isPass = false; |
| ALOGE("setLocation succeeds on bad args: (%f, %f)", atlanticLat, tooFarWest); |
| } |
| status = AMediaMuxer_setLocation(muxer, atlanticLat, tooFarEast); |
| if (status == AMEDIA_OK) { |
| isPass = false; |
| ALOGE("setLocation succeeds on bad args: (%f, %f)", atlanticLat, tooFarEast); |
| } |
| status = AMediaMuxer_setLocation(muxer, tooFarNorth, tooFarWest); |
| if (status == AMEDIA_OK) { |
| isPass = false; |
| ALOGE("setLocation succeeds on bad args: (%f, %f)", tooFarNorth, tooFarWest); |
| } |
| status = AMediaMuxer_setLocation(muxer, atlanticLat, atlanticLong); |
| isGeoDataSupported = (status == AMEDIA_OK); |
| if (isGeoDataSupported) { |
| status = AMediaMuxer_setLocation(muxer, annapurnaLat, annapurnaLong); |
| if (status != AMEDIA_OK) { |
| isPass = false; |
| ALOGE("setLocation fails on args: (%f, %f)", annapurnaLat, annapurnaLong); |
| } |
| } else { |
| isPass &= ((MuxerFormat)jformat != OUTPUT_FORMAT_MPEG_4 && |
| (MuxerFormat)jformat != OUTPUT_FORMAT_THREE_GPP); |
| } |
| const char* csrcPath = env->GetStringUTFChars(jsrcPath, nullptr); |
| auto* mediaInfo = new MuxerNativeTestHelper(csrcPath); |
| if (mediaInfo->registerTrack(muxer) && AMediaMuxer_start(muxer) == AMEDIA_OK) { |
| status = AMediaMuxer_setLocation(muxer, atlanticLat, atlanticLong); |
| if (status == AMEDIA_OK) { |
| isPass = false; |
| ALOGE("setLocation succeeds after starting the muxer"); |
| } |
| if (mediaInfo->insertSampleData(muxer) && AMediaMuxer_stop(muxer) == AMEDIA_OK) { |
| status = AMediaMuxer_setLocation(muxer, atlanticLat, atlanticLong); |
| if (status == AMEDIA_OK) { |
| isPass = false; |
| ALOGE("setLocation succeeds after stopping the muxer"); |
| } |
| } else { |
| isPass = false; |
| ALOGE("failed to writeSampleData or stop muxer"); |
| } |
| } else { |
| isPass = false; |
| ALOGE("failed to addTrack or start muxer"); |
| } |
| delete mediaInfo; |
| env->ReleaseStringUTFChars(jsrcPath, csrcPath); |
| AMediaMuxer_delete(muxer); |
| fclose(ofp); |
| } else { |
| isPass = false; |
| ALOGE("failed to open output file %s", cdstPath); |
| } |
| env->ReleaseStringUTFChars(jdstPath, cdstPath); |
| return static_cast<jboolean>(isPass); |
| } |
| |
| static jboolean nativeTestSetOrientationHint(JNIEnv* env, jobject, jint jformat, jstring jsrcPath, |
| jstring jdstPath) { |
| bool isPass = true; |
| bool isOrientationSupported; |
| const int badRotation[] = {360, 45, -90}; |
| const int oldRotation = 90; |
| const int currRotation = 180; |
| const char* cdstPath = env->GetStringUTFChars(jdstPath, nullptr); |
| FILE* ofp = fopen(cdstPath, "wbe+"); |
| if (ofp) { |
| AMediaMuxer* muxer = AMediaMuxer_new(fileno(ofp), (OutputFormat)jformat); |
| media_status_t status; |
| for (int degrees : badRotation) { |
| status = AMediaMuxer_setOrientationHint(muxer, degrees); |
| if (status == AMEDIA_OK) { |
| isPass = false; |
| ALOGE("setOrientationHint succeeds on bad args: %d", degrees); |
| } |
| } |
| status = AMediaMuxer_setOrientationHint(muxer, oldRotation); |
| isOrientationSupported = (status == AMEDIA_OK); |
| if (isOrientationSupported) { |
| status = AMediaMuxer_setOrientationHint(muxer, currRotation); |
| if (status != AMEDIA_OK) { |
| isPass = false; |
| ALOGE("setOrientationHint fails on args: %d", currRotation); |
| } |
| } else { |
| isPass &= ((MuxerFormat)jformat != OUTPUT_FORMAT_MPEG_4 && |
| (MuxerFormat)jformat != OUTPUT_FORMAT_THREE_GPP); |
| } |
| const char* csrcPath = env->GetStringUTFChars(jsrcPath, nullptr); |
| auto* mediaInfo = new MuxerNativeTestHelper(csrcPath); |
| if (mediaInfo->registerTrack(muxer) && AMediaMuxer_start(muxer) == AMEDIA_OK) { |
| status = AMediaMuxer_setOrientationHint(muxer, currRotation); |
| if (status == AMEDIA_OK) { |
| isPass = false; |
| ALOGE("setOrientationHint succeeds after starting the muxer"); |
| } |
| if (mediaInfo->insertSampleData(muxer) && AMediaMuxer_stop(muxer) == AMEDIA_OK) { |
| status = AMediaMuxer_setOrientationHint(muxer, currRotation); |
| if (status == AMEDIA_OK) { |
| isPass = false; |
| ALOGE("setOrientationHint succeeds after stopping the muxer"); |
| } |
| } else { |
| isPass = false; |
| ALOGE("failed to writeSampleData or stop muxer"); |
| } |
| } else { |
| isPass = false; |
| ALOGE("failed to addTrack or start muxer"); |
| } |
| delete mediaInfo; |
| env->ReleaseStringUTFChars(jsrcPath, csrcPath); |
| AMediaMuxer_delete(muxer); |
| fclose(ofp); |
| } else { |
| isPass = false; |
| ALOGE("failed to open output file %s", cdstPath); |
| } |
| env->ReleaseStringUTFChars(jdstPath, cdstPath); |
| return static_cast<jboolean>(isPass); |
| } |
| |
| static jboolean nativeTestMultiTrack(JNIEnv* env, jobject, jint jformat, jstring jsrcPathA, |
| jstring jsrcPathB, jstring jrefPath, jstring jdstPath) { |
| bool isPass = true; |
| const char* csrcPathA = env->GetStringUTFChars(jsrcPathA, nullptr); |
| const char* csrcPathB = env->GetStringUTFChars(jsrcPathB, nullptr); |
| auto* mediaInfoA = new MuxerNativeTestHelper(csrcPathA); |
| auto* mediaInfoB = new MuxerNativeTestHelper(csrcPathB); |
| if (mediaInfoA->getTrackCount() == 1 && mediaInfoB->getTrackCount() == 1) { |
| const char* crefPath = env->GetStringUTFChars(jrefPath, nullptr); |
| // number of times to repeat {mSrcFileA, mSrcFileB} in Output |
| int numTracks[][2]{{1, 1}, {2, 0}, {0, 2}, {1, 2}, {2, 1}}; |
| // prepare reference |
| FILE* rfp = fopen(crefPath, "wbe+"); |
| if (rfp) { |
| AMediaMuxer* muxer = AMediaMuxer_new(fileno(rfp), (OutputFormat)jformat); |
| bool muxStatus = mediaInfoA->combineMedias(muxer, mediaInfoB, numTracks[0]); |
| AMediaMuxer_delete(muxer); |
| fclose(rfp); |
| if (muxStatus) { |
| auto* refInfo = new MuxerNativeTestHelper(crefPath); |
| if (!mediaInfoA->equals(refInfo) || !mediaInfoB->equals(refInfo)) { |
| isPass = false; |
| ALOGE("testMultiTrack: inputs: %s %s, fmt: %d, error ! muxing src A and src B " |
| "failed", csrcPathA, csrcPathB, jformat); |
| } else { |
| const char* cdstPath = env->GetStringUTFChars(jdstPath, nullptr); |
| for (int i = 1; i < sizeof(numTracks) / sizeof(numTracks[0]) && isPass; i++) { |
| FILE* ofp = fopen(cdstPath, "wbe+"); |
| if (ofp) { |
| muxer = AMediaMuxer_new(fileno(ofp), (OutputFormat)jformat); |
| bool status = |
| mediaInfoA->combineMedias(muxer, mediaInfoB, numTracks[i]); |
| AMediaMuxer_delete(muxer); |
| fclose(ofp); |
| if (status) { |
| auto* dstInfo = new MuxerNativeTestHelper(cdstPath); |
| if (!dstInfo->equals(refInfo)) { |
| isPass = false; |
| ALOGE("testMultiTrack: inputs: %s %s, fmt: %d, error ! muxing " |
| "src A: %d, src B: %d failed", csrcPathA, csrcPathB, |
| jformat, numTracks[i][0], numTracks[i][1]); |
| } |
| delete dstInfo; |
| } else { |
| if ((MuxerFormat)jformat != OUTPUT_FORMAT_MPEG_4) { |
| isPass = false; |
| ALOGE("testMultiTrack: inputs: %s %s, fmt: %d, error ! muxing " |
| "src A: %d, src B: %d failed", csrcPathA, csrcPathB, |
| jformat, numTracks[i][0], numTracks[i][1]); |
| } |
| } |
| } else { |
| isPass = false; |
| ALOGE("failed to open output file %s", cdstPath); |
| } |
| } |
| env->ReleaseStringUTFChars(jdstPath, cdstPath); |
| } |
| delete refInfo; |
| } else { |
| if ((MuxerFormat)jformat != OUTPUT_FORMAT_OGG) { |
| isPass = false; |
| ALOGE("testMultiTrack: inputs: %s %s, fmt: %d, error ! muxing src A and src B " |
| "failed", csrcPathA, csrcPathB, jformat); |
| } |
| } |
| } else { |
| isPass = false; |
| ALOGE("failed to open reference output file %s", crefPath); |
| } |
| env->ReleaseStringUTFChars(jrefPath, crefPath); |
| } else { |
| isPass = false; |
| if (mediaInfoA->getTrackCount() != 1) { |
| ALOGE("error: file %s, track count exp/rec - %d/%d", csrcPathA, 1, |
| mediaInfoA->getTrackCount()); |
| } |
| if (mediaInfoB->getTrackCount() != 1) { |
| ALOGE("error: file %s, track count exp/rec - %d/%d", csrcPathB, 1, |
| mediaInfoB->getTrackCount()); |
| } |
| } |
| env->ReleaseStringUTFChars(jsrcPathA, csrcPathA); |
| env->ReleaseStringUTFChars(jsrcPathB, csrcPathB); |
| delete mediaInfoA; |
| delete mediaInfoB; |
| return static_cast<jboolean>(isPass); |
| } |
| |
| static jboolean nativeTestOffsetPts(JNIEnv* env, jobject, jint format, jstring jsrcPath, |
| jstring jdstPath, jintArray joffsetIndices) { |
| bool isPass = true; |
| const int OFFSET_TS = 111000; |
| jsize len = env->GetArrayLength(joffsetIndices); |
| jint* coffsetIndices = env->GetIntArrayElements(joffsetIndices, nullptr); |
| const char* csrcPath = env->GetStringUTFChars(jsrcPath, nullptr); |
| const char* cdstPath = env->GetStringUTFChars(jdstPath, nullptr); |
| auto* mediaInfo = new MuxerNativeTestHelper(csrcPath); |
| if (mediaInfo->getTrackCount() != 0) { |
| for (int trackID = 0; trackID < mediaInfo->getTrackCount() && isPass; trackID++) { |
| for (int i = 0; i < len; i++) { |
| mediaInfo->offsetTimeStamp(trackID, OFFSET_TS, coffsetIndices[i]); |
| } |
| FILE* ofp = fopen(cdstPath, "wbe+"); |
| if (ofp) { |
| AMediaMuxer* muxer = AMediaMuxer_new(fileno(ofp), (OutputFormat)format); |
| mediaInfo->muxMedia(muxer); |
| AMediaMuxer_delete(muxer); |
| fclose(ofp); |
| auto* outInfo = new MuxerNativeTestHelper(cdstPath); |
| isPass = mediaInfo->equals(outInfo); |
| if (!isPass) { |
| ALOGE("Validation failed after adding timestamp offset to track %d", trackID); |
| } |
| delete outInfo; |
| } else { |
| isPass = false; |
| ALOGE("failed to open output file %s", cdstPath); |
| } |
| for (int i = len - 1; i >= 0; i--) { |
| mediaInfo->offsetTimeStamp(trackID, -OFFSET_TS, coffsetIndices[i]); |
| } |
| } |
| } else { |
| isPass = false; |
| ALOGE("no valid track found in input file %s", csrcPath); |
| } |
| env->ReleaseStringUTFChars(jdstPath, cdstPath); |
| env->ReleaseStringUTFChars(jsrcPath, csrcPath); |
| env->ReleaseIntArrayElements(joffsetIndices, coffsetIndices, 0); |
| delete mediaInfo; |
| return static_cast<jboolean>(isPass); |
| } |
| |
| static jboolean nativeTestSimpleMux(JNIEnv* env, jobject, jstring jsrcPath, jstring jdstPath, |
| jstring jmime, jstring jselector) { |
| bool isPass = true; |
| const char* cmime = env->GetStringUTFChars(jmime, nullptr); |
| const char* csrcPath = env->GetStringUTFChars(jsrcPath, nullptr); |
| const char* cselector = env->GetStringUTFChars(jselector, nullptr); |
| auto* mediaInfo = new MuxerNativeTestHelper(csrcPath, cmime); |
| static const std::map<MuxerFormat, const char*> formatStringPair = { |
| {OUTPUT_FORMAT_MPEG_4, "mp4"}, |
| {OUTPUT_FORMAT_WEBM, "webm"}, |
| {OUTPUT_FORMAT_THREE_GPP, "3gp"}, |
| {OUTPUT_FORMAT_HEIF, "heif"}, |
| {OUTPUT_FORMAT_OGG, "ogg"}}; |
| if (mediaInfo->getTrackCount() == 1) { |
| const char* cdstPath = env->GetStringUTFChars(jdstPath, nullptr); |
| for (int fmt = OUTPUT_FORMAT_START; fmt <= OUTPUT_FORMAT_LIST_END && isPass; fmt++) { |
| auto it = formatStringPair.find((MuxerFormat)fmt); |
| if (it == formatStringPair.end() || strstr(cselector, it->second) == nullptr) { |
| continue; |
| } |
| if (fmt == OUTPUT_FORMAT_WEBM) continue; // TODO(b/146923551) |
| FILE* ofp = fopen(cdstPath, "wbe+"); |
| if (ofp) { |
| AMediaMuxer* muxer = AMediaMuxer_new(fileno(ofp), (OutputFormat)fmt); |
| bool muxStatus = mediaInfo->muxMedia(muxer); |
| bool result = true; |
| AMediaMuxer_delete(muxer); |
| fclose(ofp); |
| if (muxStatus) { |
| auto* outInfo = new MuxerNativeTestHelper(cdstPath, cmime); |
| result = mediaInfo->equals(outInfo); |
| delete outInfo; |
| } |
| if ((muxStatus && !result) || |
| (!muxStatus && isCodecContainerPairValid((MuxerFormat)fmt, cmime))) { |
| isPass = false; |
| ALOGE("error: file %s, mime %s, output != clone(input) for format %d", csrcPath, |
| cmime, fmt); |
| } |
| } else { |
| isPass = false; |
| ALOGE("error: file %s, mime %s, failed to open output file %s", csrcPath, cmime, |
| cdstPath); |
| } |
| } |
| env->ReleaseStringUTFChars(jdstPath, cdstPath); |
| } else { |
| isPass = false; |
| ALOGE("error: file %s, mime %s, track count exp/rec - %d/%d", csrcPath, cmime, 1, |
| mediaInfo->getTrackCount()); |
| } |
| env->ReleaseStringUTFChars(jselector, cselector); |
| env->ReleaseStringUTFChars(jsrcPath, csrcPath); |
| env->ReleaseStringUTFChars(jmime, cmime); |
| delete mediaInfo; |
| return static_cast<jboolean>(isPass); |
| } |
| |
| int registerAndroidMediaV2CtsMuxerTestApi(JNIEnv* env) { |
| const JNINativeMethod methodTable[] = { |
| {"nativeTestSetOrientationHint", "(ILjava/lang/String;Ljava/lang/String;)Z", |
| (void*)nativeTestSetOrientationHint}, |
| {"nativeTestSetLocation", "(ILjava/lang/String;Ljava/lang/String;)Z", |
| (void*)nativeTestSetLocation}, |
| }; |
| jclass c = env->FindClass("android/mediav2/cts/MuxerTest$TestApi"); |
| return env->RegisterNatives(c, methodTable, sizeof(methodTable) / sizeof(JNINativeMethod)); |
| } |
| |
| int registerAndroidMediaV2CtsMuxerTestMultiTrack(JNIEnv* env) { |
| const JNINativeMethod methodTable[] = { |
| {"nativeTestMultiTrack", |
| "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z", |
| (void*)nativeTestMultiTrack}, |
| }; |
| jclass c = env->FindClass("android/mediav2/cts/MuxerTest$TestMultiTrack"); |
| return env->RegisterNatives(c, methodTable, sizeof(methodTable) / sizeof(JNINativeMethod)); |
| } |
| |
| int registerAndroidMediaV2CtsMuxerTestOffsetPts(JNIEnv* env) { |
| const JNINativeMethod methodTable[] = { |
| {"nativeTestOffsetPts", "(ILjava/lang/String;Ljava/lang/String;[I)Z", |
| (void*)nativeTestOffsetPts}, |
| }; |
| jclass c = env->FindClass("android/mediav2/cts/MuxerTest$TestOffsetPts"); |
| return env->RegisterNatives(c, methodTable, sizeof(methodTable) / sizeof(JNINativeMethod)); |
| } |
| |
| int registerAndroidMediaV2CtsMuxerTestSimpleMux(JNIEnv* env) { |
| const JNINativeMethod methodTable[] = { |
| {"nativeTestSimpleMux", |
| "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z", |
| (void*)nativeTestSimpleMux}, |
| }; |
| jclass c = env->FindClass("android/mediav2/cts/MuxerTest$TestSimpleMux"); |
| return env->RegisterNatives(c, methodTable, sizeof(methodTable) / sizeof(JNINativeMethod)); |
| } |
| |
| extern int registerAndroidMediaV2CtsMuxerUnitTestApi(JNIEnv* env); |
| |
| extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { |
| JNIEnv* env; |
| if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) return JNI_ERR; |
| if (registerAndroidMediaV2CtsMuxerTestApi(env) != JNI_OK) return JNI_ERR; |
| if (registerAndroidMediaV2CtsMuxerTestMultiTrack(env) != JNI_OK) return JNI_ERR; |
| if (registerAndroidMediaV2CtsMuxerTestOffsetPts(env) != JNI_OK) return JNI_ERR; |
| if (registerAndroidMediaV2CtsMuxerTestSimpleMux(env) != JNI_OK) return JNI_ERR; |
| if (registerAndroidMediaV2CtsMuxerUnitTestApi(env) != JNI_OK) return JNI_ERR; |
| return JNI_VERSION_1_6; |
| } |