(frameworks/av/media/libstagefright/MPEG4Writer.cpp)
一、MPEG4Writer相关流程
MPEG4Writer是Android stagefright媒体框架下一个的封装类,录制视频调用的MediaRecorder接口类的底层封装实现通过它完成。
以视频为例,MPEG4Writer是视频录制的最后一环。
MPEG4Writer遵守 ISO 14496-12标准进行封装,MP4、3gp、ismv等我们常见的媒体封装格式都是以这种基础文件格式为基础衍生的。
Android系统录像封装流程主要有三个步骤:
1) 录制开始时,写入文件头部。
2) 录制进行时,实时写入音视频轨迹的数据块。
3) 录制结束时,写入索引信息并更新头部参数。
索引负责描述音视频轨迹的特征,会随着音视频轨迹的存储而变化,所以通常做法会将录像文件索引信息放在音视频轨迹流后面,在媒体流数据写完(录像结束)后才能写入。可以看到,存放音视频数据的mdat box是位于第二位的,而负责检索音视频的moov box是位于最后的,这与通常的MP4封装的排列顺序不同,当然这是为了符合录制而产生的结果。因为 moov的大小是随着 mdat 变化的,而我们录制视频的时间预先是不知道的,所以需要先将mdat 数据写入,最后再写入moov,完成封装。
现有Android系统上录像都是录制是MP4或3GP格式,底层就是使用MPEG4Writer组合器类来完成的,它将编码后的音视频轨迹按照MPEG4规范进行封装,填入各个参数,就组合成完整的MP4格式文件。MPEG4Writer的组合功能主要由两种线程完成,一种是负责音视频数据写入封装文件的写线程(WriterThread),一种是音视频数据读取处理的轨迹线程(TrackThread)。轨迹线程一般有两个:视频轨迹数据读取线程和音频轨迹数据读取线程,而写线程只有一个,负责将轨迹线程中打包成Chunk的数据写入封装文件。
如图2所示,轨迹线程是以帧为单位获取数据帧(Sample),并将每帧中的信息及系统环境信息提取汇总存储在内存的trak表中,其中需要维持的信息有Chunk写入文件的偏移地址Stco(Chunk Offset)、Sample与Chunk的映射关系Stsc(Sample-to-Chunk)、关键帧Stss(Sync Sample)、每一帧的持续时间Stts(Time-to-Sample)等,这些信息是跟每一帧的信息密切相关的,由图可以看出trak表由各自的线程维护,当录像结束时trak表会就会写入封装文件。而每一帧的数据流会先存入一个链表缓存中,当帧的数量达到一定值时,轨迹线程会将这些帧数据打包成块(Chunk)并通知写线程写入到封装文件。写线程接到Chunk已准备好的通知后就马上搜索Chunk链表(链表个数与轨迹线程个数相关,一般有两个,音视频轨迹线程各有一个),将找到的第一个Chunk后便写入封装文件,并会将写入的偏移地址更新到相应的trak表的Stco项(但trak表中其它数据是由轨迹线程更新)。音视频的Chunk数据是存储于同一mdat box中,按添加到Chunk链表时间先后顺序排列。等到录像结束时,录像应用会调用MPEG4Writer的stop方法,此时就会将音视频的trak表分别写入moov。
二、MPEG4Writer.cpp跟读
1、构造函数
构造函数,MPEG4Writer::MPEG4Writer,实现一些参数的初始化,fd是传进来的录制文件的文件描述符。
MPEG4Writer::MPEG4Writer(int fd) {
initInternal(fd, true /*isFirstSession*/);
}
2、start()
应用层 MediaRecorder.start();时,往framework层调用时,将会调用到MPEG4Writer.cpp 中的 start部分,在start部分,我们看到在这一部分,writeFtypBox(param) 将实现录制文件文件头部信息的相关信息的写入操作;startWriterThread() 开启封装视频文件的写线程;startTracks(param) 开启视频数据的读线程,也就是前面文件部分所说的轨迹线程。
status_t MPEG4Writer::start(MetaData *param) {
…
//暂停后,再次start时,mStarted = true;mPaused = false;
if (mStarted) {
if (mPaused) {
mPaused = false;
return startTracks(param);
}
return OK;
}
…
writeFtypBox(param); //写入封装文件头部信息
mFreeBoxOffset = mOffset;
…
mOffset = mMdatOffset;
lseek64(mFd, mMdatOffset, SEEK_SET); //将文件指针移动到mMdatOffset的位置
status_t err = startWriterThread(); //开启封装视频文件的写线程
err = startTracks(param); //开启视频数据的读线程
mStarted = true;
}
3、startWriterThread()
看下 startWriterThread()部分,在startWriterThread()函数中,将真正建立新的子线程,并在子线程中执行ThreadWrapper()函数中的操作。
status_t MPEG4Writer::startWriterThread() {
mDone = false;
mIsFirstChunk = true;
mDriftTimeUs = 0;
for (List<Track *>::iterator it = mTracks.begin(); it != mTracks.end(); ++it) {
ChunkInfo info;
info.mTrack = *it;
info.mPrevChunkTimestampUs = 0;
info.mMaxInterChunkDurUs = 0;
mChunkInfos.push_back(info); //
}
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
pthread_create(&mThread, &attr,ThreadWrapper, this);
pthread_attr_destroy(&attr);
mWriterThreadStarted = true;
return OK;
}
3.1 ThreadWrapper()
接着继续看 ThreadWrapper()函数,在这里new 了一个MPEGWriter对象,真正的操作在threadFunc()中体现。
void *MPEG4Writer::ThreadWrapper(void *me) {
MPEG4Writer *writer = static_cast<MPEG4Writer *>(me);
writer->threadFunc();
return NULL;
}
3.2 threadFun()
下面看下threadFun()。在这个函数中,将根据变量mDone 进行while循环,一直检测是否有数据块Chunk可写。轨迹线程是一直将读数据的数据往buffer中写入,buffer到了一定量后,就是chunk,这时就会通过信号量 mChunkReadyCondition来通知封装文件的写线程去检测链表,然后将检索到的Chunk数据写入文件的数据区,当然写之前,肯定会去判断下是否真的有数据可写。
void MPEG4Writer::threadFunc() {
prctl(PR_SET_NAME, (unsigned long)"MPEG4Writer", 0, 0, 0);
Mutex::Autolock autoLock(mLock);
while (!mDone) {
Chunk chunk;
bool chunkFound = false;
while (!mDone && !(chunkFound = findChunkToWrite(&chunk))) {
mChunkReadyCondition.wait(mLock);
}
// In real time recording mode, write without holding the lock in order
// to reduce the blocking time for media track threads.
// Otherwise, hold the lock until the existing chunks get written to the
// file.
if (chunkFound) {
if (mIsRealTimeRecording) {
mLock.unlock();
}
writeChunkToFile(&chunk);
if (mIsRealTimeRecording) {
mLock.lock();
}
}
}
writeAllChunks();//应用层MediaRecorder.stop()时,mDone的值将为true。
}
3.3 writerChunkToFile(&chunk)
下面看下writerChunkToFile(&chunk);轨迹线程读数据时是以数据帧Sample为单位,所以这里将Chunk写入封装文件,也是以Sample为单位,遍历整个链表,将数据写入封装文件,真正的写入操作是addSample_l(*it);
void MPEG4Writer::writeChunkToFile(Chunk* chunk) {
int32_t isFirstSample = true;
while (!chunk->mSamples.empty()) {
List<MediaBuffer *>::iterator it = chunk->mSamples.begin();
off64_t offset = chunk->mTrack->isAvc()
? addLengthPrefixedSample_l(*it)
:addSample_l(*it);
if (isFirstSample) {
chunk->mTrack->addChunkOffset(offset);
isFirstSample = false;
}
(*it)->release();
(*it) = NULL;
chunk->mSamples.erase(it);
}
chunk->mSamples.clear();
}
3.4 addSamole_l(*it)
下面看下 addSamole_l(*it) 函数,wirte写入操作,mFd 是上层设置录制的文件路径传下来的文件描述符。
off64_t MPEG4Writer::addSample_l(MediaBuffer *buffer) {
off64_t old_offset = mOffset;
::write(mFd,
(const uint8_t *)buffer->data() + buffer->range_offset(),
buffer->range_length());
mOffset += buffer->range_length();
return old_offset;
}
到此,封装文件的写入线程的操作大体走完,下面看轨迹线程的操作。
4、startTracks(param)
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);
}
}
(*it)->start(params) 将会执行status_t MPEG4Writer::Track::start(MetaData *params) {} 。在这边也是同样新建子线程,在子线程中执行轨迹线程的相应操作。
status_t MPEG4Writer::Track::start(MetaData *params) {
pthread_create(&mThread, &attr, ThreadWrapper, this);
}
真正的操作又是放到了threadEntry()中去执行。
void *MPEG4Writer::Track::ThreadWrapper(void *me) {
Track *track = static_cast<Track *>(me);
status_t err = track->threadEntry();
return (void *) err;
}
录制文件结束时,上层应用分别是调用 MediaRecorder的stop()、reset()和release()方法,下面看下MPEG4Writer.cpp中相对应的操作。
status_t MPEG4Writer::Track::stop() {
status_t status = mSource->stop();
}
reset()函数中将完成轨迹线程、写入线程的停止、封装文件尾部信息的写入等操作。
status_t MPEG4Writer::reset() {
for (List<Track *>::iterator it = mTracks.begin(); it != mTracks.end(); ++it) {
status_t status = (*it)->stop();//停止轨迹线程
}
stopWriterThread(); //停止封装文件的写线程
writeMoovBox(maxDurationUs);//封装文件尾部相应信息的写入
write("free", 4);
release();
}
release()中关闭文件描述符。
void MPEG4Writer::release() {
close(mFd);
mFd = -1;
mInitCheck = NO_INIT;
mStarted = false;
}
至此,整个录制文件的封装过程,大体流程走完,就是有轨迹线程不停的读取数据,然后读取的数据达到一定的大小时,也就是成chunk(块)时,将会通知写入线程去检测是否有chunk可写,有的话,将进行数据的写入操作。整个封装文件必须有相应的头部信息和尾部信息。
参考: