| /* |
| * 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. |
| */ |
| |
| //#define LOG_NDEBUG 0 |
| #define LOG_TAG "MediaAppender" |
| |
| #include <media/stagefright/MediaAppender.h> |
| #include <media/stagefright/MediaCodec.h> |
| #include <media/stagefright/foundation/ABuffer.h> |
| #include <utils/Log.h> |
| // TODO : check if this works for NDK apps without JVM |
| // #include <media/ndk/NdkJavaVMHelperPriv.h> |
| |
| namespace android { |
| |
| struct MediaAppender::sampleDataInfo { |
| size_t size; |
| int64_t time; |
| size_t exTrackIndex; |
| sp<MetaData> meta; |
| }; |
| |
| sp<MediaAppender> MediaAppender::create(int fd, AppendMode mode) { |
| if (fd < 0) { |
| ALOGE("invalid file descriptor"); |
| return nullptr; |
| } |
| if (!(mode >= APPEND_MODE_FIRST && mode <= APPEND_MODE_LAST)) { |
| ALOGE("invalid mode %d", mode); |
| return nullptr; |
| } |
| sp<MediaAppender> ma = new (std::nothrow) MediaAppender(fd, mode); |
| if (ma->init() != OK) { |
| return nullptr; |
| } |
| return ma; |
| } |
| |
| // TODO: inject mediamuxer and mediaextractor objects. |
| // TODO: @format is not required as an input if we can sniff the file and find the format of |
| // the existing content. |
| // TODO: Code it to the interface(MediaAppender), and have a separate MediaAppender NDK |
| MediaAppender::MediaAppender(int fd, AppendMode mode) |
| : mFd(fd), |
| mMode(mode), |
| // TODO : check if this works for NDK apps without JVM |
| // mExtractor(new NuMediaExtractor(NdkJavaVMHelper::getJNIEnv() != nullptr |
| // ? NuMediaExtractor::EntryPoint::NDK_WITH_JVM |
| // : NuMediaExtractor::EntryPoint::NDK_NO_JVM)), |
| mExtractor(new (std::nothrow) NuMediaExtractor(NuMediaExtractor::EntryPoint::NDK_WITH_JVM)), |
| mTrackCount(0), |
| mState(UNINITIALIZED) { |
| ALOGV("MediaAppender::MediaAppender mode:%d", mode); |
| } |
| |
| status_t MediaAppender::init() { |
| std::scoped_lock lock(mMutex); |
| ALOGV("MediaAppender::init"); |
| status_t status = mExtractor->setDataSource(mFd, 0, lseek(mFd, 0, SEEK_END)); |
| if (status != OK) { |
| ALOGE("extractor_setDataSource failed, status :%d", status); |
| return status; |
| } |
| |
| sp<AMessage> fileFormat; |
| status = mExtractor->getFileFormat(&fileFormat); |
| if (status != OK) { |
| ALOGE("extractor_getFileFormat failed, status :%d", status); |
| return status; |
| } |
| |
| AString fileMime; |
| fileFormat->findString("mime", &fileMime); |
| // only compare the end of the file MIME type to allow for vendor customized mime type |
| if (fileMime.endsWith("mp4")){ |
| mFormat = MediaMuxer::OUTPUT_FORMAT_MPEG_4; |
| } else { |
| ALOGE("Unsupported file format, extractor name:%s, fileformat %s", |
| mExtractor->getName(), fileMime.c_str()); |
| return ERROR_UNSUPPORTED; |
| } |
| |
| mTrackCount = mExtractor->countTracks(); |
| ALOGV("mTrackCount:%zu", mTrackCount); |
| if (mTrackCount == 0) { |
| ALOGE("no tracks are present"); |
| return ERROR_MALFORMED; |
| } |
| size_t exTrackIndex = 0; |
| ssize_t audioTrackIndex = -1, videoTrackIndex = -1; |
| bool audioSyncSampleTimeSet = false; |
| |
| while (exTrackIndex < mTrackCount) { |
| sp<AMessage> fmt; |
| status = mExtractor->getTrackFormat(exTrackIndex, &fmt, 0); |
| if (status != OK) { |
| ALOGE("getTrackFormat failed for trackIndex:%zu, status:%d", exTrackIndex, status); |
| return status; |
| } |
| AString mime; |
| if (fmt->findString("mime", &mime)) { |
| if (!strncasecmp(mime.c_str(), "video/", 6)) { |
| ALOGV("VideoTrack"); |
| if (videoTrackIndex != -1) { |
| ALOGE("Not more than one video track is supported"); |
| return ERROR_UNSUPPORTED; |
| } |
| videoTrackIndex = exTrackIndex; |
| } else if (!strncasecmp(mime.c_str(), "audio/", 6)) { |
| ALOGV("AudioTrack"); |
| if (audioTrackIndex != -1) { |
| ALOGE("Not more than one audio track is supported"); |
| } |
| audioTrackIndex = exTrackIndex; |
| } else { |
| ALOGV("Neither Video nor Audio track"); |
| } |
| } |
| mFmtIndexMap.emplace(exTrackIndex, fmt); |
| mSampleCountVect.emplace_back(0); |
| mMaxTimestampVect.emplace_back(0); |
| mLastSyncSampleTimeVect.emplace_back(0); |
| status = mExtractor->selectTrack(exTrackIndex); |
| if (status != OK) { |
| ALOGE("selectTrack failed for trackIndex:%zu, status:%d", exTrackIndex, status); |
| return status; |
| } |
| ++exTrackIndex; |
| } |
| |
| ALOGV("AudioTrackIndex:%zu, VideoTrackIndex:%zu", audioTrackIndex, videoTrackIndex); |
| |
| do { |
| sampleDataInfo tmpSDI; |
| // TODO: read info into members of the struct sampleDataInfo directly |
| size_t sampleSize; |
| status = mExtractor->getSampleSize(&sampleSize); |
| if (status != OK) { |
| ALOGE("getSampleSize failed, status:%d", status); |
| return status; |
| } |
| mSampleSizeVect.emplace_back(sampleSize); |
| tmpSDI.size = sampleSize; |
| int64_t sampleTime = 0; |
| status = mExtractor->getSampleTime(&sampleTime); |
| if (status != OK) { |
| ALOGE("getSampleTime failed, status:%d", status); |
| return status; |
| } |
| mSampleTimeVect.emplace_back(sampleTime); |
| tmpSDI.time = sampleTime; |
| status = mExtractor->getSampleTrackIndex(&exTrackIndex); |
| if (status != OK) { |
| ALOGE("getSampleTrackIndex failed, status:%d", status); |
| return status; |
| } |
| mSampleIndexVect.emplace_back(exTrackIndex); |
| tmpSDI.exTrackIndex = exTrackIndex; |
| ++mSampleCountVect[exTrackIndex]; |
| mMaxTimestampVect[exTrackIndex] = std::max(mMaxTimestampVect[exTrackIndex], sampleTime); |
| sp<MetaData> sampleMeta; |
| status = mExtractor->getSampleMeta(&sampleMeta); |
| if (status != OK) { |
| ALOGE("getSampleMeta failed, status:%d", status); |
| return status; |
| } |
| mSampleMetaVect.emplace_back(sampleMeta); |
| int32_t val = 0; |
| if (sampleMeta->findInt32(kKeyIsSyncFrame, &val) && val != 0) { |
| mLastSyncSampleTimeVect[exTrackIndex] = sampleTime; |
| } |
| tmpSDI.meta = sampleMeta; |
| mSDI.emplace_back(tmpSDI); |
| } while (mExtractor->advance() == OK); |
| |
| mExtractor.clear(); |
| |
| std::sort(mSDI.begin(), mSDI.end(), [](sampleDataInfo& a, sampleDataInfo& b) { |
| int64_t aOffset, bOffset; |
| a.meta->findInt64(kKeySampleFileOffset, &aOffset); |
| b.meta->findInt64(kKeySampleFileOffset, &bOffset); |
| return aOffset < bOffset; |
| }); |
| for (int64_t syncSampleTime : mLastSyncSampleTimeVect) { |
| ALOGV("before ignoring frames, mLastSyncSampleTimeVect:%lld", (long long)syncSampleTime); |
| } |
| ALOGV("mMode:%u", mMode); |
| if (mMode == APPEND_MODE_IGNORE_LAST_VIDEO_GOP && videoTrackIndex != -1 ) { |
| ALOGV("Video track is present"); |
| bool lastVideoIframe = false; |
| size_t lastVideoIframeOffset = 0; |
| int64_t lastVideoSampleTime = -1; |
| for (auto rItr = mSDI.rbegin(); rItr != mSDI.rend(); ++rItr) { |
| if (rItr->exTrackIndex != videoTrackIndex) { |
| continue; |
| } |
| if (lastVideoSampleTime == -1) { |
| lastVideoSampleTime = rItr->time; |
| } |
| int64_t offset = 0; |
| if (!rItr->meta->findInt64(kKeySampleFileOffset, &offset) || offset == 0) { |
| ALOGE("Missing offset"); |
| return ERROR_MALFORMED; |
| } |
| ALOGV("offset:%lld", (long long)offset); |
| int32_t val = 0; |
| if (rItr->meta->findInt32(kKeyIsSyncFrame, &val) && val != 0) { |
| ALOGV("sampleTime:%lld", (long long)rItr->time); |
| ALOGV("lastVideoSampleTime:%lld", (long long)lastVideoSampleTime); |
| if (lastVideoIframe == false && (lastVideoSampleTime - rItr->time) > |
| 1000000/* Track interleaving duration in MPEG4Writer*/) { |
| ALOGV("lastVideoIframe got chosen"); |
| lastVideoIframe = true; |
| mLastSyncSampleTimeVect[videoTrackIndex] = rItr->time; |
| lastVideoIframeOffset = offset; |
| ALOGV("lastVideoIframeOffset:%lld", (long long)offset); |
| break; |
| } |
| } |
| } |
| if (lastVideoIframe == false) { |
| ALOGV("Need to rewrite all samples"); |
| mLastSyncSampleTimeVect[videoTrackIndex] = 0; |
| lastVideoIframeOffset = 0; |
| } |
| unsigned int framesIgnoredCount = 0; |
| for (auto itr = mSDI.begin(); itr != mSDI.end();) { |
| int64_t offset = 0; |
| ALOGV("trackIndex:%zu, %" PRId64 "", itr->exTrackIndex, itr->time); |
| if (itr->meta->findInt64(kKeySampleFileOffset, &offset) && |
| offset >= lastVideoIframeOffset) { |
| ALOGV("offset:%lld", (long long)offset); |
| if (!audioSyncSampleTimeSet && audioTrackIndex != -1 && |
| audioTrackIndex == itr->exTrackIndex) { |
| mLastSyncSampleTimeVect[audioTrackIndex] = itr->time; |
| audioSyncSampleTimeSet = true; |
| } |
| itr = mSDI.erase(itr); |
| ++framesIgnoredCount; |
| } else { |
| ++itr; |
| } |
| } |
| ALOGV("framesIgnoredCount:%u", framesIgnoredCount); |
| } |
| |
| if (mMode == APPEND_MODE_IGNORE_LAST_VIDEO_GOP && videoTrackIndex == -1 && |
| audioTrackIndex != -1) { |
| ALOGV("Only AudioTrack is present"); |
| for (auto rItr = mSDI.rbegin(); rItr != mSDI.rend(); ++rItr) { |
| int32_t val = 0; |
| if (rItr->meta->findInt32(kKeyIsSyncFrame, &val) && val != 0) { |
| mLastSyncSampleTimeVect[audioTrackIndex] = rItr->time; |
| break; |
| } |
| } |
| unsigned int framesIgnoredCount = 0; |
| for (auto itr = mSDI.begin(); itr != mSDI.end();) { |
| if (itr->time >= mLastSyncSampleTimeVect[audioTrackIndex]) { |
| itr = mSDI.erase(itr); |
| ++framesIgnoredCount; |
| } else { |
| ++itr; |
| } |
| } |
| ALOGV("framesIgnoredCount :%u", framesIgnoredCount); |
| } |
| |
| for (size_t i = 0; i < mLastSyncSampleTimeVect.size(); ++i) { |
| ALOGV("mLastSyncSampleTimeVect[%zu]:%lld", i, (long long)mLastSyncSampleTimeVect[i]); |
| mFmtIndexMap[i]->setInt64( |
| "sample-time-before-append" /*AMEDIAFORMAT_KEY_SAMPLE_TIME_BEFORE_APPEND*/, |
| mLastSyncSampleTimeVect[i]); |
| } |
| for (size_t i = 0; i < mMaxTimestampVect.size(); ++i) { |
| ALOGV("mMaxTimestamp[%zu]:%lld", i, (long long)mMaxTimestampVect[i]); |
| } |
| for (size_t i = 0; i < mSampleCountVect.size(); ++i) { |
| ALOGV("SampleCountVect[%zu]:%zu", i, mSampleCountVect[i]); |
| } |
| mState = INITIALIZED; |
| return OK; |
| } |
| |
| MediaAppender::~MediaAppender() { |
| ALOGV("MediaAppender::~MediaAppender"); |
| mMuxer.clear(); |
| mExtractor.clear(); |
| } |
| |
| status_t MediaAppender::start() { |
| std::scoped_lock lock(mMutex); |
| ALOGV("MediaAppender::start"); |
| if (mState != INITIALIZED) { |
| ALOGE("MediaAppender::start() is called in invalid state %d", mState); |
| return INVALID_OPERATION; |
| } |
| mMuxer = MediaMuxer::create(mFd, mFormat); |
| if (mMuxer == nullptr) { |
| ALOGE("MediaMuxer::create failed"); |
| return INVALID_OPERATION; |
| } |
| for (const auto& n : mFmtIndexMap) { |
| ssize_t muxIndex = mMuxer->addTrack(n.second); |
| if (muxIndex < 0) { |
| ALOGE("addTrack failed"); |
| return UNKNOWN_ERROR; |
| } |
| mTrackIndexMap.emplace(n.first, muxIndex); |
| } |
| ALOGV("trackIndexmap size:%zu", mTrackIndexMap.size()); |
| |
| status_t status = mMuxer->start(); |
| if (status != OK) { |
| ALOGE("muxer start failed:%d", status); |
| return status; |
| } |
| |
| ALOGV("Sorting samples based on their offsets"); |
| for (int i = 0; i < mSDI.size(); ++i) { |
| ALOGV("i:%d", i + 1); |
| /* TODO : Allocate a single allocation of the max size, and reuse it across ABuffers if |
| * using new ABuffer(void *, size_t). |
| */ |
| sp<ABuffer> data = new (std::nothrow) ABuffer(mSDI[i].size); |
| if (data == nullptr) { |
| ALOGE("memory allocation failed"); |
| return NO_MEMORY; |
| } |
| data->setRange(0, mSDI[i].size); |
| int32_t val = 0; |
| int sampleFlags = 0; |
| if (mSDI[i].meta->findInt32(kKeyIsSyncFrame, &val) && val != 0) { |
| sampleFlags |= MediaCodec::BUFFER_FLAG_SYNCFRAME; |
| } |
| |
| int64_t val64; |
| if (mSDI[i].meta->findInt64(kKeySampleFileOffset, &val64)) { |
| ALOGV("SampleFileOffset Found :%zu:%lld:%lld", mSDI[i].exTrackIndex, |
| (long long)mSampleCountVect[mSDI[i].exTrackIndex], (long long)val64); |
| sp<AMessage> bufMeta = data->meta(); |
| bufMeta->setInt64("sample-file-offset" /*AMEDIAFORMAT_KEY_SAMPLE_TIME_BEFORE_APPEND*/, |
| val64); |
| } |
| if (mSDI[i].meta->findInt64(kKeyLastSampleIndexInChunk, &val64)) { |
| ALOGV("kKeyLastSampleIndexInChunk Found %lld:%lld", |
| (long long)mSampleCountVect[mSDI[i].exTrackIndex], (long long)val64); |
| sp<AMessage> bufMeta = data->meta(); |
| bufMeta->setInt64( |
| "last-sample-index-in-chunk" /*AMEDIAFORMAT_KEY_LAST_SAMPLE_INDEX_IN_CHUNK*/, |
| val64); |
| } |
| status = mMuxer->writeSampleData(data, mTrackIndexMap[mSDI[i].exTrackIndex], mSDI[i].time, |
| sampleFlags); |
| if (status != OK) { |
| ALOGE("muxer writeSampleData failed:%d", status); |
| return status; |
| } |
| } |
| mState = STARTED; |
| return OK; |
| } |
| |
| status_t MediaAppender::stop() { |
| std::scoped_lock lock(mMutex); |
| ALOGV("MediaAppender::stop"); |
| if (mState == STARTED) { |
| status_t status = mMuxer->stop(); |
| if (status != OK) { |
| mState = ERROR; |
| } else { |
| mState = STOPPED; |
| } |
| return status; |
| } else { |
| ALOGE("stop() is called in invalid state %d", mState); |
| return INVALID_OPERATION; |
| } |
| } |
| |
| ssize_t MediaAppender::getTrackCount() { |
| std::scoped_lock lock(mMutex); |
| ALOGV("MediaAppender::getTrackCount"); |
| if (mState != INITIALIZED && mState != STARTED) { |
| ALOGE("getTrackCount() is called in invalid state %d", mState); |
| return -1; |
| } |
| return mTrackCount; |
| } |
| |
| sp<AMessage> MediaAppender::getTrackFormat(size_t idx) { |
| std::scoped_lock lock(mMutex); |
| ALOGV("MediaAppender::getTrackFormat"); |
| if (mState != INITIALIZED && mState != STARTED) { |
| ALOGE("getTrackFormat() is called in invalid state %d", mState); |
| return nullptr; |
| } |
| if (idx < 0 || idx >= mTrackCount) { |
| ALOGE("getTrackFormat() idx is out of range"); |
| return nullptr; |
| } |
| return mFmtIndexMap[idx]; |
| } |
| |
| status_t MediaAppender::writeSampleData(const sp<ABuffer>& buffer, size_t trackIndex, |
| int64_t timeUs, uint32_t flags) { |
| std::scoped_lock lock(mMutex); |
| ALOGV("writeSampleData:trackIndex:%zu, time:%" PRId64 "", trackIndex, timeUs); |
| return mMuxer->writeSampleData(buffer, trackIndex, timeUs, flags); |
| } |
| |
| status_t MediaAppender::setOrientationHint([[maybe_unused]] int degrees) { |
| ALOGE("setOrientationHint not supported. Has to be called prior to start on initial muxer"); |
| return ERROR_UNSUPPORTED; |
| }; |
| |
| status_t MediaAppender::setLocation([[maybe_unused]] int latit, [[maybe_unused]] int longit) { |
| ALOGE("setLocation not supported. Has to be called prior to start on initial muxer"); |
| return ERROR_UNSUPPORTED; |
| } |
| |
| ssize_t MediaAppender::addTrack([[maybe_unused]] const sp<AMessage> &format) { |
| ALOGE("addTrack not supported"); |
| return ERROR_UNSUPPORTED; |
| } |
| |
| } // namespace android |