blob: 2d9c651786444be9cb37beedf9c0c28df6a505f1 [file] [log] [blame] [edit]
/*
* 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