若有需要请查看前面章节分析:
【一】Android MediaRecorder整体架构源码浅析
【二】Android MediaRecorder C++底层架构音视频处理过程和音视频同步源码分析
【三】Android MediaRecorder C++底层架构音视频处理过程和音视频同步源码分析
【四】Android MediaRecorder C++底层架构音视频处理过程和音视频同步源码分析
前面已经分析到:MPEG4Writer::Track::threadEntry()方法通过循环read()读取到了编码器编码后的音视频数据,再继续往下分析:
For循环中代码处理片段分析:
1、当读取到的Buffer数据长度为0时,丢弃该数据并释放内存,并且记录0长度帧个数,这种情况在我们前面的分析过程中其实是有的。
if (buffer->range_length() == 0) {
buffer->release();
buffer = NULL;
++nZeroLengthFrames;
continue;
}
2、这个看英文注释就比较清楚了,即如果Track轨道跟踪数据被暂停了,那么此时收到的数据就会被丢弃释放掉。
// If the codec specific data has not been received yet, delay pause.
// After the codec specific data is received, discard what we received
// when the track is to be paused.
if (mPaused && !mResumed) {
buffer->release();
buffer = NULL;
continue;
}
3、写入MP4格式的数据:
if (mIsMPEG4) {
copyCodecSpecificData((const uint8_t *)buffer->data() + buffer->range_offset(),
buffer->range_length());
}
status_t MPEG4Writer::Track::copyCodecSpecificData(
const uint8_t *data, size_t size, size_t minLength) {
if (size < minLength) {
ALOGE("Codec specific data length too short: %zu", size);
return ERROR_MALFORMED;
}
mCodecSpecificData = malloc(size);
if (mCodecSpecificData == NULL) {
ALOGE("Failed allocating codec specific data");
return NO_MEMORY;
}
mCodecSpecificDataSize = size;
// 最终将所有编码后的数据都存入了mCodecSpecificData变量中:,
// 即 void *mCodecSpecificData;
memcpy(mCodecSpecificData, data, size);
return OK;
}
然后释放拷贝的编码后buffer即:
buffer->release();
buffer = NULL;
但若上面的【kKeyIsCodecConfig】没有设置
则继续往下分析:
4、每帧元数据样本的大小必须小于允许的最大值,否则释放内存并且停止录制
// Per-frame metadata sample's size must be smaller than max allowed.
if (!mIsVideo && !mIsAudio && buffer->range_length() >= kMaxMetadataSize) {
ALOGW("Buffer size is %zu. Maximum metadata buffer size is %lld for %s track",
buffer->range_length(), (long long)kMaxMetadataSize, trackName);
buffer->release();
mSource->stop();
mIsMalformed = true;
break;
}
// 包含非csd数据的帧(非0长度)个数
++nActualFrames;
5、此处主要就是拷贝或直接使用缓冲区的数据操作
MediaBuffer *copy = NULL;
// 检查上游源是否提示可以保留缓冲区而不立即释放,并避免拷贝缓冲区
// Check if the upstream source hints it is OK to hold on to the
// buffer without releasing immediately and avoid cloning the buffer
if (AVUtils::get()->canDeferRelease(buffer->meta_data())) {
copy = buffer;
meta_data = new MetaData(*buffer->meta_data().get());
} else {
// Make a deep copy of the MediaBuffer and Metadata and release
// the original as soon as we can
copy = new MediaBuffer(buffer->range_length());
memcpy(copy->data(), (uint8_t *)buffer->data() + buffer->range_offset(),
buffer->range_length());
copy->set_range(0, buffer->range_length());
meta_data = new MetaData(*buffer->meta_data().get());
buffer->release();
buffer = NULL;
}
6、接着分析: 这一段代码分析的是文件最大大小和最大录制时长的处理,并且文件达到最大大小后,会启动用户先前已经设置的新的文件,不停止录制的情况下继续向新文件中写入编码后数据。
// Max file size or duration handling
mMdatSizeBytes += sampleSize;
// 估算 【mEstimatedTrackSizeBytes】总共已Track追踪到的数据大小
updateTrackSizeEstimate();
// 下面会分析exceedsFileSizeLimit())和switchFd()
if (mOwner->exceedsFileSizeLimit()) {
if (mOwner->switchFd() != OK) {
ALOGW("Recorded file size exceeds limit %" PRId64 "bytes",
mOwner->mMaxFileSizeLimitBytes);
mSource->stop();
mOwner->notify(
MEDIA_RECORDER_EVENT_INFO, MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED, 0);
} else {
ALOGV("%s Current recorded file size exceeds limit %" PRId64 "bytes. Switching output",
getTrackType(), mOwner->mMaxFileSizeLimitBytes);
}
// 疑问:释放本次的buffer数据,感觉这个buffer音视频数据会被丢弃造成缺帧??
copy->release();
break;
}
// 下面也会分析超出最大限制录制时间时的处理,若是超出限制时间则立即停止录制和是否该buffer数据内存。
if (mOwner->exceedsFileDurationLimit()) {
ALOGW("Recorded file duration exceeds limit %" PRId64 "microseconds",
mOwner->mMaxFileDurationLimitUs);
mOwner->notify(MEDIA_RECORDER_EVENT_INFO, MEDIA_RECORDER_INFO_MAX_DURATION_REACHED, 0);
copy->release();
mSource->stop();
break;
}
// 估算文件大小将要达到最大size时发出通知给java层
if (mOwner->approachingFileSizeLimit()) {
mOwner->notifyApproachingLimit();
}
先分析:exceedsFileSizeLimit()
bool MPEG4Writer::exceedsFileSizeLimit() {
// No limit
if (mMaxFileSizeLimitBytes == 0) {
return false;
}
估算目前MPEG4格式数据的moovBox结构数据的size大小
int64_t nTotalBytesEstimate = static_cast<int64_t>(mEstimatedMoovBoxSize);
for (List<Track *>::iterator it = mTracks.begin();
it != mTracks.end(); ++it) {
// 再加上每个Track轨道计算出来的各自track数据大小,得到最终总的MPEG4数据大小
nTotalBytesEstimate += (*it)->getEstimatedTrackSizeBytes();
}
if (!mStreamableFile) {
// Add 1024 bytes as error tolerance
return nTotalBytesEstimate + 1024 >= mMaxFileSizeLimitBytes;
}
// 然后判断若是大于文件最大限制大小的95%,则认为超出文件限制大小了
// Be conservative in the estimate: do not exceed 95% of
// the target file limit. For small target file size limit, though,
// this will not help.
return (nTotalBytesEstimate >= (95 * mMaxFileSizeLimitBytes) / 100);
}
分析switchFd():
status_t MPEG4Writer::switchFd() {
ALOGV("switchFd");
Mutex::Autolock l(mLock);
mSwitchPending = true;
// 发送一个【kWhatSwitch】事件给自己
sp<AMessage> msg = new AMessage(kWhatSwitch, mReflector);
status_t err = msg->post();
return err;
}
然后分析MPEG4Writer::onMessageReceived方法中的该事件处理为:
case kWhatSwitch:
{
/**
该方法主要调用stopWriterThread();结束写入线程并且也调用Track的stop(stopSource);方法,但不停止音视频源的编码数据处理流程,让其能够继续编码数据,然后write各种Box结构数据到最终的文件中,最后release()掉:mMoovBoxBuffer = NULL;数据
**/
finishCurrentSession();
mLock.lock();
// 切换了新的文件描述符来继续写入后续编码的数据
int fd = mNextFd;
mNextFd = -1;
mLock.unlock();
// MPEG4Writer的内部重新初始化一些数据,然后会调用:Track的resetInternal();方法
initInternal(fd, false /*isFirstSession*/);
// 然后重新开始Write处理流程
start(mStartMeta.get());
mSwitchPending = false;
// 通知java层该事件
notify(MEDIA_RECORDER_EVENT_INFO, MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED, 0);
break;
}
分析mOwner->exceedsFileDurationLimit():
bool MPEG4Writer::exceedsFileDurationLimit() {
// No limit
if (mMaxFileDurationLimitUs == 0) {
return false;
}
for (List<Track *>::iterator it = mTracks.begin();
it != mTracks.end(); ++it) {
// 只要有一个Track的追踪数据时间达到最大限制录制时间时就返回true
if ((*it)->getDurationUs() >= mMaxFileDurationLimitUs) {
return true;
}
}
return false;
}
7、继续往下分析
int32_t isSync = false;
meta_data->findInt32(kKeyIsSyncFrame, &isSync);
CHECK(meta_data->findInt64(kKeyTime, ×tampUs));
// For video, skip the first several non-key frames until getting the first key frame.
if (mIsVideo && !mGotStartKeyFrame && !isSync) {
// 视频数据时第一帧若不是关键帧数据则跳过并释放其数据内存
ALOGD("Video skip non-key frame");
copy->release();
continue;
}
if (mIsVideo && isSync) {
mGotStartKeyFrame = true;
}
8、初始化刚开始的时间戳
if (mStszTableEntries->count() == 0) {
Mutex::Autolock _l(mTrackLock);
mFirstSampleTimeRealUs = systemTime() / 1000;
mStartTimestampUs = timestampUs;
mOwner->setStartTimestampUs(mStartTimestampUs);
previousPausedDurationUs = mStartTimestampUs;
}
TimestampDebugHelperEntry timestampDebugEntry;
// 这里有个重要的时间戳有效数据的验证,即这两个变量应该是大于等于0,否则就停止
timestampUs -= previousPausedDurationUs;
timestampDebugEntry.pts = timestampUs;
if (WARN_UNLESS(timestampUs >= 0ll, "for %s track", trackName)) {
copy->release();
mSource->stop();
mIsMalformed = true;
break;
}
9、分析:如果是视频帧数据
if (mIsVideo) {
/*
* Composition time: timestampUs
* Decoding time: decodingTimeUs
* Composition time offset = composition time - decoding time
*/
int64_t decodingTimeUs;
CHECK(meta_data->findInt64(kKeyDecodingTime, &decodingTimeUs));
decodingTimeUs -= previousPausedDurationUs;
// 省略部分代码
if (mStszTableEntries->count() == 0) {
/**
强制第一个ctts表条目只有一个条目,这样我们就可以很容易在writeCttsBox()中对初始轨道启动进行时间偏移调整。【英文注释大概意思】
**/
// Force the first ctts table entry to have one single entry
// so that we can do adjustment for the initial track start
// time offset easily in writeCttsBox().
lastCttsOffsetTimeTicks = currCttsOffsetTimeTicks;
addOneCttsTableEntry(1, currCttsOffsetTimeTicks);
cttsSampleCount = 0; // No sample in ctts box is pending
} else {
if (currCttsOffsetTimeTicks != lastCttsOffsetTimeTicks) {
addOneCttsTableEntry(cttsSampleCount, lastCttsOffsetTimeTicks);
lastCttsOffsetTimeTicks = currCttsOffsetTimeTicks;
cttsSampleCount = 1; // One sample in ctts box is pending
} else {
++cttsSampleCount;
}
}
// 更新ctts时间偏移范围
// Update ctts time offset range
if (mStszTableEntries->count() == 0) {
mMinCttsOffsetTicks = currCttsOffsetTimeTicks;
mMaxCttsOffsetTicks = currCttsOffsetTimeTicks;
} else {
if (currCttsOffsetTimeTicks > mMaxCttsOffsetTicks) {
mMaxCttsOffsetTicks = currCttsOffsetTimeTicks;
} else if (currCttsOffsetTimeTicks < mMinCttsOffsetTicks) {
mMinCttsOffsetTicks = currCttsOffsetTimeTicks;
mMinCttsOffsetTimeUs = cttsOffsetTimeUs;
}
}
}
10、默认是true,则若是音频数据,则更新音频元数据中的偏移时间戳
if (mOwner->isRealTimeRecording()) {
if (mIsAudio) {
updateDriftTime(meta_data);
}
}
11、轨道追踪到的持续时间戳记录
if (timestampUs > mTrackDurationUs) {
mTrackDurationUs = timestampUs;
}
12、先Stsz表中添加当前样本大小数据
mStszTableEntries->add(htonl(sampleSize));
if (mStszTableEntries->count() > 2) {
/** 非常重要的音视频同步处理:强制第一个样本数据拥有它自己的stts条目,以便稍后我们可以调整它的值以保持A/V同步。
**/
// Force the first sample to have its own stts entry so that
// we can adjust its value later to maintain the A/V sync.
if (mStszTableEntries->count() == 3 || currDurationTicks != lastDurationTicks) {
addOneSttsTableEntry(sampleCount, lastDurationTicks);
sampleCount = 1;
} else {
// 样本数据个数增加
++sampleCount;
}
}
13、当前buffer数据的时间戳处理:
// 记录当前buffer录制的持续时长
lastDurationUs = timestampUs - lastTimestampUs;
lastDurationTicks = currDurationTicks;
lastTimestampUs = timestampUs;
14、最重要的处理,即将copy的buffer数据放入到列表集合中
mChunkSamples.push_back(copy);
/**
该参数是:const int64_t interleaveDurationUs = mOwner->interleaveDuration();
即获取到的是比之前的那个buffer离开的间隔持续时长
**/
if (interleaveDurationUs == 0) {
addOneStscTableEntry(++nChunks, 1);
// 下面进行分析
bufferChunk(timestampUs);
} else {
if (chunkTimestampUs == 0) {
chunkTimestampUs = timestampUs;
} else {
int64_t chunkDurationUs = timestampUs - chunkTimestampUs;
if (chunkDurationUs > interleaveDurationUs) {
if (chunkDurationUs > mMaxChunkDurationUs) {
mMaxChunkDurationUs = chunkDurationUs;
}
++nChunks;
if (nChunks == 1 || // First chunk
lastSamplesPerChunk != mChunkSamples.size()) {
lastSamplesPerChunk = mChunkSamples.size();
addOneStscTableEntry(nChunks, lastSamplesPerChunk);
}
// 下面进行分析
bufferChunk(timestampUs);
chunkTimestampUs = timestampUs;
}
}
}
分析:bufferChunk(timestampUs);
void MPEG4Writer::Track::bufferChunk(int64_t timestampUs) {
ALOGV("bufferChunk");
/** 可以看到此处是将Track自身实例、当前buffer时间戳和当前buffer的列表集合数据封装成Chunk块数据结构:
struct Chunk {
Track *mTrack; // Owner
int64_t mTimeStampUs; // Timestamp of the 1st sample
List<MediaBuffer *> mSamples; // Sample data
// Convenient constructor
Chunk(): mTrack(NULL), mTimeStampUs(0) {}
Chunk(Track *track, int64_t timeUs, List<MediaBuffer *> samples)
: mTrack(track), mTimeStampUs(timeUs), mSamples(samples) {
}
};
**/
Chunk chunk(this, timestampUs, mChunkSamples);
// 然后调用MPEG4Writer::bufferChunk方法
mOwner->bufferChunk(chunk);
// 最后Track将自身的该列表集合清空,以便后续的不断的读取编码后的数据存储
mChunkSamples.clear();
}
分析MPEG4Writer::bufferChunk方法:
void MPEG4Writer::bufferChunk(const Chunk& chunk) {
ALOGV("bufferChunk: %p", chunk.mTrack);
Mutex::Autolock autolock(mLock);
CHECK_EQ(mDone, false);
// mChunkInfos变量前面已经分析过来,即记录每个Track对应的ChunkInfo数据在集合中
for (List<ChunkInfo>::iterator it = mChunkInfos.begin();
it != mChunkInfos.end(); ++it) {
/** 此处重要:是用来寻找到当前chunk数据对应的的Track的ChunkInfo数据信息,然后将其放入ChunkInfo数据结构中的mChunks数据中,即每个Track可以记录存储多个Chunk来分块处理音视频数据
**/
if (chunk.mTrack == it->mTrack) {
// Found owner
it->mChunks.push_back(chunk);
/** 然后此处用于唤醒等在该条件信号量上的wait事件,即根据前面分析过的,此wait事件是在MPEG4Writer::threadFunc()方法中的:
**/
mChunkReadyCondition.signal();
return;
}
}
CHECK(!"Received a chunk for a unknown track");
}
总结上面函数:如此上面该方法就完成了把音视频数据封装成对应Track信息持有的Chunk块集合数据中,然后唤醒等待在该条件信号量上的write的处理流程:
即是Track将编码后的音视频数据进行封装成一个个的Chunk块数据结构缓存在list集合中,然后通知MPEG4Writer的写入线程进行写入文件操作了。
而MPEG4Writer的写入线程进行写入文件的操作在方法MPEG4Writer::threadFunc()
中进行的,而该方法在前面的分析过程中已经分析过了,就是读取对应Track中的Chunk集合数据是否有数据了,有就进行写入文件操作。
简要总结:
如上,本系列章节分析已结束,基本已经比较清晰明了的完成了如下分析:
1、 音视频原始数据分别的start开启流程;
2、 MPEG4Writer开启独立线程进行拉取音视频编码后的Chunk数据集合进行写入文件;
3、 Track将编码后的MediaBuffer结构数据封装成了Chunk块结构数据,并放入了各自Track持有的【mChunks】集合数据中;
4、 分别分析了音频和视频原始数据从Driver或服务器端的回调数据流程,和中间各种数据类型进行转换封装等过程;
5、 对整个音视频原始数据从获取到编码再到写入文件,进行了比较详细的源码分析;
6、 音视频处理过程用到了非常多的条件锁信号量机制来进行写入数据和读取数据的异步变成同步通知问题,即消费者和生产者模式,但不同的是生产者不会停止等待,因为buffer内存只要足够就能往里面写入buffer数据。
7、 等等
下面只是简要记录流程的总结,不太完整:
(XXX)、音视频处理流程及其同步总结简要记录调用及其处理关系分析:
1、MPEG4Writer.start()——》startWriterThread() 开启写入线程 —》 线程调用void MPEG4Writer::threadFunc() 独立线程中并且加锁后可一直循环获取和处理MediaBuffer类型的数据直到完成或结束(加锁原因:音视频多通道Track数据都在此处循环进行匹配处理)
—》void MPEG4Writer::writeChunkToFile(Chunk* chunk)可能加锁写入(真实时间录制模式不加锁)通过块数据写入
—》通过参数的chunk->mSamples 【List<MediaBuffer *> mSamples; // Sample data 样本数据 媒体缓冲数据区】
遍历所有的MediaBuffer样本缓冲区数据并写入【off64_t MPEG4Writer::addSample_l(MediaBuffer *buffer) 】目标文件,
–》void MediaBuffer::release() 释放销毁缓冲区 —》 mObserver->signalBufferReturned(this);
最后调用 writeAllChunks();
ListTableEntries.write()写入beginBox(“stsc”);【void MPEG4Writer::Track::writeStscBox() 】。
使用Condition条件锁即信号量机制来同步push buffer和pull buffer。
2、status_t MPEG4Writer::startTracks(MetaData *params) 开始追踪音视频源数据【其实此处writer获取到的是encoder已经编码过后的数据了,encoder才是处理原始数据的】
----》遍历所有的音视频源轨道mTracks并调用track.start(params); 与上面的不在同一个线程中 【mTracks是 MPEG4Writer::addSource添加新的track对象的】
即status_t MPEG4Writer::Track::start(MetaData *params),每个Track内部都开启一个新线程来追踪处理各自声道源的数据即音视频不同轨道的数据源
—》【encoder】mSource->start(meta.get()); 此句是encoder【MediaCodecSource】的start方法 并后面接着开启新线程后调用status_t MPEG4Writer::Track::threadEntry()
–》【encoder】mSource->read(&buffer)) 【 MediaBuffer *buffer;】读入编码后的数据到buffer中,以便后面的流程读取使用,
—》 mChunkSamples.push_back(copy);【MediaBuffer copy】—> bufferChunk(timestampUs);
3、【encoder】mSource->start(meta.get()); ----》 此句会最后通过Handler机制调用status_t MediaCodecSource::onStart(MetaData *params) 方法
—》接着通过Handler机制调用void MediaCodecSource::Puller::onMessageReceived(const sp &msg)触发【new CameraSource】cameraSource.start()方法,
—》 接着后面Handler机制调用 【CameraSource】mSource->read(&mbuf); 此处获取的数据源是CameraSource中编码前的原始数据,
—》 接着没有数据时调用Puller.handleEOS();
4、
Handler开始事件流程简要分析记录:
kWhatStart —》kWhatPull —》
start方法分析:
kWhatStart: 【CameraSource】mSource->start(MetaData meta) 方法,—》 status_t CameraSource::startCameraRecording() 执行,
–》 initBufferQueue() 【Initialize buffer queue.】初始化
void BufferQueue::createBufferQueue(sp outProducer,
sp* outConsumer,
const sp& allocator)
Create memory heap to store buffers as VideoNativeMetadata. 主要功能是这个。
然后调用进行IPC通信让Camera进行录制,并通过回调来处理事件。BpCameraRecordingProxy发送【START_RECORDING】事件给Bn代理
mCameraRecordingProxy->startRecording(
new ProxyListener(this)))
然后通过listener回调void CameraSource::ProxyListener::dataCallbackTimestamp()再调用
void CameraSource::dataCallbackTimestamp(int64_t timestampUs,
int32_t msgType __unused, const sp &data) 【有加锁】
mFramesReceived.push_back(data);
mFrameTimes.push_back(timeUs);
mFrameAvailableCondition.signal(); 最后通知未编码的原始数据已经接收完成,而该通知会在read()【有加锁】方法中的锁唤醒进行。如下第5点分析。
5、
read方法分析:
kWhatPull: 【CameraSource】mSource->read(&mbuf); 【if we need to discard buffer 里面做了丢弃media buffer 或者,
if simply paused, keep pulling source 暂停时保持继续获取数据源】
被上面的回调唤醒后进行,读取IMemory的列表帧frame数据【包括frame帧数据即IMemory数据和frameTime帧时间数据】,
然后将其编码【mFramesBeingEncoded.push_back(frame);】后,将编码后的数据封装成一个MediaBuffer **buffer,即返回给了上层调用者!
*buffer = new MediaBuffer(frame->pointer(), frame->size());
(*buffer)->setObserver(this);
(*buffer)->add_ref();
(*buffer)->meta_data()->setInt64(kKeyTime, frameTime);
【mFramesBeingEncoded.push_back(frame);】这个应该理解成将要被编码的原始数据,而不是已经正在被编码的数据。
该列表数据会在void CameraSource::signalBufferReturned(MediaBuffer *buffer) 方法中进行匹配frame数据后将其从列表数据中删除,然后通过条件锁通知完成事件。
最后通过【status_t MediaCodecSource::feedEncoderInputBuffers()】(该方法时通过Handler机制发送kWhatPullerNotify或kWhatEncoderActivity回调的,此处为kWhatPullerNotify)
内部调用mPuller->readBuffer(&mbuf)去使用该buffer从Vector<MediaBuffer *> mReadBuffers;缓存区中获取音视频未编码原始数据,然后后续将其送入encoder进行编码,关键如下三句:
获取输入缓冲区
status_t err = mEncoder->getInputBuffer(bufferIndex, &inbuf);
从源source【mbuf】中拷贝size个字节到目标destin[inbuf]中
即将未编码数据拷贝并放入待编码的缓冲区
memcpy(inbuf->data(), mbuf->data(), size);
重新将输入缓冲区交给Encoder使用,让其继续编码并编码后放入输出缓冲区中
status_t err = mEncoder->queueInputBuffer(bufferIndex, 0, size, timeUs, flags);
然后通过Handler机制调用onQueueInputBuffer(msg);【此处可有CryptoPlugin::Mode mode = CryptoPlugin::kMode_Unencrypted;加密处理】
非常重要的一个多媒体缓冲区队列结构体:
struct Queue {
Queue()
: mReadPendingSince(0),
mPaused(false),
mPulling(false) {
}
int64_t mReadPendingSince;
bool mPaused;
bool mPulling;
Vector<MediaBuffer *> mReadBuffers;
void flush(); ====》刷新缓冲区数据源
// if queue is empty, return false and set *|buffer| to NULL . Otherwise, pop 【pop 弹出】
// buffer from front of the queue, place it into *|buffer| and return true. 【 true代表有数据读取成功】
bool readBuffer(MediaBuffer **buffer); 获取读取缓冲区数据
// add a buffer to the back of the queue
void pushBuffer(MediaBuffer *mbuf); 添加一个缓冲区数据到队尾
};
【一】Android MediaRecorder整体架构源码浅析
此前第一章节分析的简要总结分析:MPEG4Writer读取的是最终encoder(MediaCodec【MediaCodecSource中初始化并使用的与OpenMAX组件继续通信的编解码器】)编码器进过编码后的数据,而encoder将CameraSource从driver层传递过来的源音视频数据进行编码等操作。
最终核心处理类是MPEG4Writer和Track对象,Track内部最终通过dump和MPEG4Writer调用write函数,将最终编码后的数据写入了文件,调用了文件写入函数write函数【::write(fd, result.string(), result.size());】处理的。
因此对于音视频两个轨道的元数据,进行了分Track进行记录和处理的:
for (List<Track *>::iterator it = mTracks.begin();
it != mTracks.end(); ++it) {
(*it)->dump(fd, args);
}
直接遍历进行了数据在文件中进行合并了。
调用关系:
StagefrightRecorder::dump()->MPEG4Writer.dump()->遍历Track.dump(),最终写入文件数据。
MediaRecorder本系列章节内容分析已在一两年前分析完成的,当初只是用于分析的笔记记录下来,如今已走上了我向往的音视频开发领域,也调研和参与过音视频相关技术,目前喜爱C++底层音视频技术的开发,偏向于底层的音视频复用/解复用、解码/编码、推流拉流技术等,而当初分析本章内容时音视频技术积累较少,因此若本文中分析有误还请多多指教,谢谢。
过了这么久时间才分享出来的原因是,以往技术知识也向前辈们分享文章有所收获,现如今我也可以有所技术分享给需要的人,也希望可以帮忙到需要的人,技术分享帮助他人的同时也能作为自身技术掌握的总结记录。
以往忙于技术深入和研究未有所分享的技术,往后坚持有质量的技术分享。