MediaRecorder录制
前言
没有看过MediaRecorder其他的容器的代码,MPEG4Writer有看过一点点,就用MPEG4Writer为例来分析一下流程。其中以Audio的流程为主
MPEG4Writer是Android stagefright媒体框架下一个的封装类,我们平常录制视频调用的MediaRecorder接口类其视频录制的底层封装实现就是通过MPEG4Writer完成的,以视频为例,MPEG4Writer是视频录制的最后一环。
MediaRecorder框架
先从框架上来看一下MediaRecorder的调用流程
MPEG4Writer遵守 ISO 14496-12标准进行封装,在现有Android系统上录像都是录制是MP4或3GP格式,底层就是使用MPEG4Writer组合器类来完成的,它将编码后的音视频轨迹按照MPEG4规范进行封装,填入各个参数,就组合成完整的MP4格式文件。
Android系统录像封装流程主要有三个步骤:
- 录制开始时,写入文件头部。
- 录制进行时,实时写入音视频轨迹的数据块。
- 录制结束时,写入索引信息并更新头部参数。
索引负责描述音视频轨迹的特征,会随着音视频轨迹的存储而变化,所以通常做法会将录像文件索引信息放在音视频轨迹流后面,在媒体流数据写完(录像结束)后才能写入。
MPEG4Writer的组合功能主要由两种线程完成,一种是负责音视频数据写入封装文件的写线程(WriterThread),一种是音视频数据读取处理的轨迹线程(TrackThread)。
轨迹线程一般有两个:视频轨迹数据读取线程和音频轨迹数据读取线程,
而写线程只有一个,负责将轨迹线程中打包成Chunk的数据写入封装文件。
轨迹线程是以帧为单位获取数据帧(Sample),并将每帧中的信息及系统环境信息提取汇总存储在内存的trak表中,其中需要维持的信息由数据块Chunk写入文件的偏移地址Stco(Chunk Offset)、Sample与Chunk的映射关系Stsc(Sample-to-Chunk)、关键帧Stss(Sync Sample)、每一帧的持续时间Stts(Time-to-Sample)等,这些信息是跟每一帧的信息密切相关的,当录像结束时trak表会就会写入封装文件。
MediaRecorder录制流程
先看下流程图
第一部分,建立编码器:
如果mVideoSource == VIDEO_SOURCE_SURFACE
那么就是在prepare()的时候建立,反之是在start()的时候建立。大体流程为
StagefrightRecorder.cpp —> prepareInternal() —> setupMPEG4orWEBMRecording
—> mp4writer = new MPEG4Writer(mOutputFd);
—> setupMediaSource(&mediaSource);
—> setupVideo/AudioEncoder(mediaSource, &encoder); //encoder为mediacodecsource编码器
—> mPuller = new Puller(source);
—> writer->addSource(encoder);
MPEG4Writer.cpp —> MPEG4Writer::addSource(const sp &source)
—> Track *track = new Track(this, source, 1 + mTracks.size()); //source为encoder编码器,准备好音视频的首个元数据轨道
第二部分,启动编码器:
StagefrightRecorder.cpp —> StagefrightRecorder::start()
—> setupMPEG4orWEBMMetaData(&meta); —> mWriter->start(meta.get());
MPEG4Writer.cpp —> MPEG4Writer::start(MetaData *param);
—> writeFtypBox(param); // 实现录制文件文件头部信息的相关信息的写入操作
—> seek64(mFd, mMdatOffset, SEEK_SET); //将文件指针移动到mMdatOffset的位置
—> startWriterThread(); // 开启封装视频文件的写线程
—> startTracks(param);开启视频数据的读线程,也就是前面文件部分所说的轨迹线程 —> (*it)->start(params) // 也就是MPEG4Writer::Track::start
1、 封装视频文件的写线程 startWriterThread()
1.1 在该函数内部,在内部会真正建立新的子线程,并在子线程中执行ThreadWrapper()函数中的操作pthread_create(&mThread, &attr, ThreadWrapper, this);
。 ThreadWrapper()函数中new 了一个MPEG4Writer对象,真正的操作在threadFunc()中体现。
void *MPEG4Writer::ThreadWrapper(void *me) {
ALOGV("ThreadWrapper: %p", me);
MPEG4Writer *writer = static_cast<MPEG4Writer *>(me);
writer->threadFunc();
return NULL;
}
1.2 在threadFunc()这个函数中,将根据变量mDone (MediaRecorder.stop()时,mDone的值将为true)进行while循环,一直检测是否有数据块Chunk可写findChunkToWrite(&chunk)
。轨迹线程是一直将读数据的数据往buffer中写入,buffer到了一定量后,就是chunk,这时就会通过信号量 mChunkReadyCondition
来通知封装文件的写线程去检测链表,然后将检索到的Chunk数据写入文件的数据区也就是writeChunkToFile(&chunk);
,当然写之前,肯定会去判断下是否真的有数据可写。
1.3 writeChunkToFile(Chunk* chunk)
轨迹线程读数据时是以数据帧Sample为单位,所以这里将Chunk写入封装文件,也是以Sample为单位,遍历整个链表,将数据写入封装文件,真正的写入操作是addSample_l(*it)
void MPEG4Writer::writeChunkToFile(Chunk* chunk) {
ALOGV("writeChunkToFile: %" PRId64 " from %s track",
chunk->mTimeStampUs, chunk->mTrack->getTrackType());
int32_t isFirstSample = true;
while (!chunk->mSamples.empty()) {
List<MediaBuffer *>::iterator it = chunk->mSamples.begin();
int32_t isExif;
if (!(*it)->meta_data().findInt32(kKeyIsExif, &isExif)) {
isExif = 0;
}
bool usePrefix = chunk->mTrack->usePrefix() && !isExif;
size_t bytesWritten;
off64_t offset = addSample_l(*it, usePrefix, isExif, &bytesWritten);
if (chunk->mTrack->isHeic()) {
chunk->mTrack->addItemOffsetAndSize(offset, bytesWritten, isExif);
} else if (isFirstSample) {
chunk->mTrack->addChunkOffset(offset);
isFirstSample = false;
}
(*it)->release();
(*it) = NULL;
chunk->mSamples.erase(it);
}
chunk->mSamples.clear();
}
1.4 我们看到addSamole_l(*it) 函数,wirte写入操作,mFd 是上层设置录制的文件路径传下来的文件描述符。
off64_t MPEG4Writer::addSample_l(
MediaBuffer *buffer, bool usePrefix, bool isExif, size_t *bytesWritten) {
off64_t old_offset = mOffset;
if (usePrefix) {
addMultipleLengthPrefixedSamples_l(buffer);
} else {
if (isExif) {
::write(mFd, &kTiffHeaderOffset, 4); // exif_tiff_header_offset field
mOffset += 4;
}
::write(mFd,
(const uint8_t *)buffer->data() + buffer->range_offset(),
buffer->range_length());
mOffset += buffer->range_length();
}
*bytesWritten = mOffset - old_offset;
return old_offset;
}
封装文件的写入线程的操作大体就是这个样子
2、 startTracks(param) 轨迹线程
2.1 startTracks(param)
文件的录制过程中是有2条轨迹线程,一个是视频的轨迹线程,另一条则是音频的轨迹线程,在starTrack(param)中是在for 循环中start了两条轨迹线程
status_t MPEG4Writer::startTracks(MetaData *params) {
for (List<Track *>::iterator it = mTracks.begin();
it != mTracks.end(); ++it) {
status_t err = (*it)->start(params);
if (err != OK) {
for (List<Track *>::iterator it2 = mTracks.begin();
it2 != it; ++it2) {
(*it2)->stop();
}
return err;
}
}
return OK;
}
(*it)->start(params) 将会执行status_t MPEG4Writer::Track::start(MetaData *params) {} 。在这边也是同样新建子线程,在子线程中执行轨迹线程的相应操作,真正的操作是放到了MPEG4Writer::Track::threadEntry()
中去执行。
MPEG4Writer.cpp —>startTracks(MetaData *params) —> MPEG4Writer::Track::start(MetaData *params) //这里面主要分两部分
—> mSource->start(meta.get()) //动mediacodecsource编码器 —> MediaCodecSource::start(MetaData* params) // 这个地方的Source 分别对应 camerasource和audiosource —> 发送消息kWhatStart —> MediaCodecSource::onStart(MetaData *params) —> 发送消息kWhatPullerNotify 并且执行 mPuller->start(meta.get(), notify)
—> pthread_create(&mThread, &attr, ThreadWrapper, this); //创建数据线程读取编码数据并写入临时的buffer —> *MPEG4Writer::Track::ThreadWrapper —> track->threadEntry() 其中threadEntry()函数主要为:
status_t MPEG4Writer::Track::threadEntry() {
while (!mDone && (err = mSource->read(&buffer)) == OK) { //从编码器一直读取编码好的数据
MediaBuffer *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()); //数据拷贝到本地的buffer
mChunkSamples.push_back(copy); // 将数据保存到mChunkSamples 并会通过bufferChunk将数据保存到ChunkInfo中然后mChunkReadyCondition.signal()
}
}
第三部分,启动数据源:
在第二部分的时候我们有看到开启轨迹线程的时候有个mSource->start(meta.get()),我们主要通过AudioSource来看下如何启动数据源,AudioSource.cpp —> mRecord->start(); 。在这个地方,我们看下关于mRecord的构造方法
mRecord = new AudioRecord(
inputSource, sampleRate, AUDIO_FORMAT_PCM_16_BIT,
audio_channel_in_mask_from_count(channelCount),
opPackageName,
(size_t) (bufCount * frameCount),
AudioRecordCallbackFunction,
this,
frameCount /*notificationFrames*/,
AUDIO_SESSION_ALLOCATE,
AudioRecord::TRANSFER_DEFAULT,
AUDIO_INPUT_FLAG_NONE,
uid,
pid,
NULL /*pAttributes*/,
selectedDeviceId);
mInitCheck = mRecord->initCheck();
其中我们看到有个AudioRecordCallbackFunction
函数,这个函数是一个回调函数callback_t
,这个函数会注册到audioflinger中,然后在AudioRecordCallbackFunction中通过source->dataCallback(*((AudioRecord::Buffer *) info)); 拿到对应的AudioRecord的录取的音频数据。
status_t AudioSource::dataCallback(const AudioRecord::Buffer& audioBuffer) {
MediaBuffer *buffer = new MediaBuffer(bufferSize);
memcpy((uint8_t *) buffer->data(),
audioBuffer.i16, audioBuffer.size);
buffer->set_range(0, bufferSize);
queueInputBuffer_l(buffer, timeUs);
return OK;
}
其中queueInputBuffer_l(buffer, timeUs);
会将buffer数据存入List<MediaBuffer * > mBuffersReceived中,然后通过
mFrameAvailableCondition.signal()交给read函数处理
第四部分,获取到数据:
在第二部分启动轨迹线程的时候,mPuller->start(meta.get(), notify); —> MediaCodecSource::Puller::start —> 发送消息kWhatStart —> 当mSource->start(meta.get())成功之后会schedulePull()—> 发送消息kWhatPull —> 收到kWhatPull之后
case kWhatPull:
{
queue.unlock();
MediaBufferBase *mbuf = NULL;
status_t err = mSource->read(&mbuf);
queue.lock();
break;
}
我们可以看到mSource->read(&mbuf);这里就将第三部分mBuffersReceived中的数据读取到
第五部分:
在第二部分MediaCodecSource::onStart的时候还发送了一个kWhatPullerNotify消息,收到消息后会通知编码器往输入buffer读取数据
—> feedEncoderInputBuffers(); —> mPuller->readBuffer(&mbuf) —> queue->readBuffer(mbuf); //这儿的数据是第四部分的mReadBuffers获取的数据
—> feedEncoderInputBuffers(); —>size_t bufferIndex = *mAvailEncoderInputIndices.begin(); //获取编码器输入bufferindex,并填充输入buffer数据 —>
sp<MediaCodecBuffer> inbuf;
status_t err = mEncoder->getInputBuffer(bufferIndex, &inbuf);
memcpy(inbuf->data(), mbuf->data(), size);
status_t err = mEncoder->queueInputBuffer(bufferIndex, 0, size, timeUs, flags);
编码器接收到数据,并编码完成会返回并回调事件:kWhatEncoderActivity,在这里处理如下事情:
1、MediaCodec::CB_INPUT_AVAILABLE 继续往inputbuffer装数据,调用feedEncoderInputBuffers接口
2、MediaCodec::CB_OUTPUT_AVAILABLE 编码器输出buffer有数据了,可以读取编码好的数据了,数据存放到了output buffer queue。
sp outbuf;
status_t err = mEncoder->getOutputBuffer(index, &outbuf);
MediaBufferBase *mbuf = new MediaBuffer(outbuf->size());
memcpy(mbuf->data(), outbuf->data(), outbuf->size());
output->mBufferQueue.push_back(mbuf);
output->mCond.signal();
mEncoder->releaseOutputBuffer(index);
第六部分,将数据写入文件:
track线程从output buffer queue读取编码好的数据存入 mSource->read(&buffer),第二步在read接口完成编码好数据读取到mChunkSamples,然后writer->threadFunc再将mChunkSamples写入到文件中。