Android framework源码MPEG4Writer.cpp学习

(frameworks/av/media/libstagefright/MPEG4Writer.cpp)

一、MPEG4Writer相关流程

MPEG4Writer是Android stagefright媒体框架下一个的封装类,录制视频调用的MediaRecorder接口类的底层封装实现通过它完成。

以视频为例,MPEG4Writer是视频录制的最后一环。

图 1 MediaRecorder调用流程

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。

图 2 MPEG4Writer封装数据流图

二、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可写,有的话,将进行数据的写入操作。整个封装文件必须有相应的头部信息和尾部信息。

 

参考:

https://blog.csdn.net/liwendovo/article/details/8478259

http://www.voidcn.com/article/p-sdudnogn-bce.html

发布了25 篇原创文章 · 获赞 5 · 访问量 6407

猜你喜欢

转载自blog.csdn.net/qq_35413770/article/details/104336283