android mpeg2ts 流媒体打包MediaMuxer 和 录制MPEG2TSWriter 以及抽帧MPEG2TSExtractor
目前android上,录相大多是mp4的视频,这在一般情况下,已经够用了。但是在一些特定的场景,比如远程临控录相或者行车记录仪上,用mp4录相,就不太理想了。为什么呢?因为远程录相,或者行车记录仪上都有一个共同的问题,那就是录相有可能中断。比如突然撞车了,或者是远程监控断电了,如果这时录的是Mp4的视频,那么就会导致,因为没有来得及写mp4的文件头信息,从而打不开视频。所以在远程监控录相和行车记录仪上,录相的格式,最好使用mpeg2ts流。
现在android无论是8.0还是9.0、10.0上,都支持录制mpeg2ts流视频,但是却不支持用MediaMuxer的writeSampleData去打包mpeg2ts。这就导致了一个问题,比如有的app上,想将一个mp4的视频,转成mpeg2ts流的视频,就无法在java端完成。且现在android8.1(9.0、10.0上没有试),录下来的mpeg2ts流,经常会丢帧,最后几帧录不下来。这个Mpeg2Ts功能显得很鸡肋。下面,我们就来讨论一下,怎么去解决这些问题。
先说一下mpeg2ts录相丢帧的问题。mpeg2ts录相的framework层cpp文件是frameworks\av\media\libstagefrightMPEG2TSWriter.cpp这一个。写数据的函数是:MPEG2TSWriter::onMessageReceived(const sp &msg) 这个函数:
void MPEG2TSWriter::onMessageReceived(const sp &msg) { switch (msg->what()) { case kWhatSourceNotify: { int32_t sourceIndex; CHECK(msg->findInt32("source-index", &sourceIndex)); sp source = mSources.editItemAt(sourceIndex); int32_t what; CHECK(msg->findInt32("what", &what)); if (what == SourceInfo::kNotifyReachedEOS || what == SourceInfo::kNotifyStartFailed) { source->setEOSReceived(); sp buffer = source->lastAccessUnit(); source->setLastAccessUnit(NULL); if (buffer != NULL) { writeTS(); writeAccessUnit(sourceIndex, buffer); } ++mNumSourcesDone; } else if (what == SourceInfo::kNotifyBuffer) { sp buffer; CHECK(msg->findBuffer("buffer", &buffer)); CHECK(source->lastAccessUnit() == NULL); int32_t oob; if (msg->findInt32("oob", &oob) && oob) { // This is codec specific data delivered out of band. // It can be written out immediately. writeTS(); writeAccessUnit(sourceIndex, buffer); break; } // We don't just write out data as we receive it from // the various sources. That would essentially write them // out in random order (as the thread scheduler determines // how the messages are dispatched). // Instead we gather an access unit for all tracks and // write out the one with the smallest timestamp, then // request more data for the written out track. // Rinse, repeat. // If we don't have data on any track we don't write // anything just yet. source->setLastAccessUnit(buffer); ALOGV("lastAccessUnitTimeUs[%d] = %.2f secs", sourceIndex, source->lastAccessUnitTimeUs() / 1E6); int64_t minTimeUs = -1; size_t minIndex = 0; for (size_t i = 0; i < mSources.size(); ++i) { const sp &source = mSources.editItemAt(i); if (source->eosReceived()) { continue; } int64_t timeUs = source->lastAccessUnitTimeUs(); if (timeUs < 0) { minTimeUs = -1; break; } else if (minTimeUs < 0 || timeUs < minTimeUs) { minTimeUs = timeUs; minIndex = i; } } if (minTimeUs < 0) { ALOGV("not all tracks have valid data."); break; } ALOGV("writing access unit at time %.2f secs (index %zu)", minTimeUs / 1E6, minIndex); source = mSources.editItemAt(minIndex); buffer = source->lastAccessUnit(); source->setLastAccessUnit(NULL); writeTS(); writeAccessUnit(minIndex, buffer); source->readMore(); } break; } default: TRESPASS(); }}
在这个函数里,收到数据,并写入到文件的是“what == SourceInfo::kNotifyBuffer”这个条件下的代码段。注意在这个代码段里的那个for循环,我们丢帧就是在这里丢的。
这个for循环的作用是干什么呢?它的作用是,选取当前录制的视频的几个源中,时间戳最小的那一个源的数据,并将选取的源的数据写入文件。这么做的原因上面的注释写了,大意是,一个视频会有几个源,分属不同的线程。因为在不同的线程,所以调度时间有先后顺序,有时视频数据已经读取到了,但是cpu现在调度的是音频源,视频数据就要等音频源写完数据后,再去写视频源的数据,这样就会导致声音和视频有错位。比如播放的时候,声音说完了,对应的画面过了一秒才播出来。
google的这个解释,似乎说的通,似乎有那么一丝的道理。但是实际上,这段代码逻辑却是有混乱不堪。再举个例子,比如当前收到的是视频帧,视频帧的timeUS,也就是时间戳是112233。然后第一次执行完这个for循环后,minTimeUs会等于112233,i=1。然后因为还有音频,会第二次执行这个for循环。假设这时音频的时间戳是112232,它比视频的时间戳小,那么,minTimeUS就被改成了112232, minIndex=2。好了,执行完上面两次for循环后,会马上执行source = mSources.editItemAt(minIndex);,去取出音频的数据,写入文件,然后再紧接着调用source->readMore();去继续读取音频的内容。
不知道大家有没有注意到,本来这次发送SourceInfo::kNotifyBuffer这个整个的源是视频源,但是到最后,写入的数据却是音频源的。那么视频源的数据到哪里去了呢?没错,居然被直接丢弃掉了,丢弃掉了........
不知道写这段逻辑的人的脑子是怎么长的,总之,这里的逻辑是个很明显的错误。想要解决这个问题也很简单,把这个fro循环去掉,SourceInfo::kNotifyBuffer这个源是谁发过来的,就写谁的数据,不用去管时间戳。因为mpeg2ts流数据,每个pes数据包中,都包含了它的时间戳。具体的可以看下面的代码:
void MPEG2TSWriter::writeAccessUnit( int32_t sourceIndex, const sp &accessUnit) { ........ int64_t timeUs; CHECK(accessUnit->meta()->findInt64("timeUs", &timeUs)); uint32_t PTS = (timeUs * 9ll) / 100ll; ........}
再者,在视频文件里,无论是哪种格式的,音频轨和视频轨都是分开存放的。接收存储数据时,只用管当前轨道的数据是按先后顺序存放的就可以。 所以,根本不需要多此一举,在收到某个源的数据后,还要和其他源的数据比时间戳。修改后的代码如下:
void MPEG2TSWriter::onMessageReceived(const sp &msg) { switch (msg->what()) { case kWhatSourceNotify: { int32_t sourceIndex; CHECK(msg->findInt32("source-index", &sourceIndex)); sp source = mSources.editItemAt(sourceIndex); int32_t what; CHECK(msg->findInt32("what", &what)); if (what == SourceInfo::kNotifyReachedEOS || what == SourceInfo::kNotifyStartFailed) { source->setEOSReceived(); sp buffer = source->lastAccessUnit(); source->setLastAccessUnit(NULL); if (buffer != NULL) { writeTS(); writeAccessUnit(sourceIndex, buffer); } ++mNumSourcesDone; } else if (what == SourceInfo::kNotifyBuffer) { sp buffer; CHECK(msg->findBuffer("buffer", &buffer)); CHECK(source->lastAccessUnit() == NULL); int32_t oob; if (msg->findInt32("oob", &oob) && oob) { // This is codec specific data delivered out of band. // It can be written out immediately. writeTS(); writeAccessUnit(sourceIndex, buffer); break; } // We don't just write out data as we receive it from // the various sources. That would essentially write them // out in random order (as the thread scheduler determines // how the messages are dispatched). // Instead we gather an access unit for all tracks and // write out the one with the smallest timestamp, then // request more data for the written out track. // Rinse, repeat. // If we don't have data on any track we don't write // anything just yet. source->setLastAccessUnit(buffer);#if 0 ALOGV("lastAccessUnitTimeUs[%d] = %.2f secs", sourceIndex, source->lastAccessUnitTimeUs() / 1E6); int64_t minTimeUs = -1; size_t minIndex = 0; for (size_t i = 0; i < mSources.size(); ++i) { const sp &source = mSources.editItemAt(i); if (source->eosReceived()) { continue; } int64_t timeUs = source->lastAccessUnitTimeUs(); if (timeUs < 0) { minTimeUs = -1; break; } else if (minTimeUs < 0 || timeUs < minTimeUs) { minTimeUs = timeUs; minIndex = i; } } if (minTimeUs < 0) { ALOGV("not all tracks have valid data."); break; } ALOGV("writing access unit at time %.2f secs (index %zu)", minTimeUs / 1E6, minIndex); source = mSources.editItemAt(minIndex);#endif buffer = source->lastAccessUnit(); source->setLastAccessUnit(NULL); writeTS(); //writeAccessUnit(minIndex, buffer); writeAccessUnit(sourceIndex, buffer); source->readMore(); } break; } default: TRESPASS(); }}
好了,上面这样修改后,经过反复测试验证,录下来的视频不存在丢帧的问题,丢帧的问题完美的解决了。
现在再来说说,怎么去提供mpeg2ts流的mediamuxer给java层使用。先上一段java上的测试代码:
MediaExtractor extractor;int trackCount;MediaMuxer muxer;HashMap indexMap;private void cloneMediaUsingMuxer(FileDescriptor srcMedia, String dstMediaPath, int expectedTrackCount, int degrees, int fmt) throws IOException {// Set up MediaExtractor to read from the source.extractor = new MediaExtractor();extractor.setDataSource(srcMedia, 0, testFileLength);trackCount = extractor.getTrackCount();muxer = new MediaMuxer(dstMediaPath, fmt); indexMap = new HashMap(trackCount);for (int i = 0; i < trackCount; i++) {extractor.selectTrack(i);MediaFormat format = extractor.getTrackFormat(i);int dstIndex = muxer.addTrack(format);indexMap.put(i, dstIndex);}if (degrees >= 0) {muxer.setOrientationHint(degrees);}muxer.start(); Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() {// Copy the samples from MediaExtractor to MediaMuxer.boolean sawEOS = false;int bufferSize = MAX_SAMPLE_SIZE;int frameCount = 0;int offset = 100;ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize);BufferInfo bufferInfo = new BufferInfo(); while (!sawEOS) { bufferInfo.offset = offset; bufferInfo.size = extractor.readSampleData(dstBuf, offset); if (bufferInfo.size < 0) { sawEOS = true; bufferInfo.size = 0; } else { bufferInfo.presentationTimeUs = extractor.getSampleTime(); bufferInfo.flags = extractor.getSampleFlags(); int trackIndex = extractor.getSampleTrackIndex(); muxer.writeSampleData(indexMap.get(trackIndex), dstBuf, bufferInfo); extractor.advance(); frameCount++; } } muxer.stop(); muxer.release(); } //这里延时10毫秒执行,是因为mpeg2ts的muxer有时启动稍慢。如果writeSampleData的 //的时候,muxer还没启动,就会报错 }, 10);return;}
这段代码中,就做了一件事,那就是从给定的文件里,用MediaExtractor去抽出每一帧,然后再用MediaMuxer将抽出的帧,打包成指定格式的视频文件。我们的目的是将一个给定的视频,通过mediaMuxer打包成mpeg2ts流视频。但是从frameworks\base\media\java\android\media\MediaMuxer.java的setUpMediaMuxer里可以看出,目前android不支持转成mpeg2ts流。要想达到我们的目的,首先需要在setUpMediaMuxer这个函数里,将mpeg2ts格式给加上去。
public static final class OutputFormat { /* Do not change these values without updating their counterparts * in include/media/stagefright/MediaMuxer.h! */ private OutputFormat() {} /** MPEG4 media file format*/ public static final int MUXER_OUTPUT_MPEG_4 = 0; /** WEBM media file format*/ public static final int MUXER_OUTPUT_WEBM = 1; /** 3GPP media file format*/ public static final int MUXER_OUTPUT_3GPP = 2;public static final int MUXER_OUTPUT_MPEG2TS = 3; }; private void setUpMediaMuxer(@NonNull FileDescriptor fd, @Format int format) throws IOException { if (format != OutputFormat.MUXER_OUTPUT_MPEG_4 && format != OutputFormat.MUXER_OUTPUT_WEBM && format != OutputFormat.MUXER_OUTPUT_3GPP && format != OutputFormat.MUXER_OUTPUT_MPEG2TS) { throw new IllegalArgumentException("format: " + format + " is invalid"); } mNativeObject = nativeSetup(fd, format); mState = MUXER_STATE_INITIALIZED; mCloseGuard.open("release"); }
在上面,我们新增了一个格式MUXER_OUTPUT_MPEG2TS 。然后这里就一步步的调到了frameworks\av\media\libstagefright\MediaMuxer.cpp,同样,我们需要在这个文件里,增加我们的格式:
enum OutputFormat { OUTPUT_FORMAT_MPEG_4 = 0, OUTPUT_FORMAT_WEBM = 1, OUTPUT_FORMAT_THREE_GPP = 2, //add by mpeg2ts OUTPUT_FORMAT_YUNOVO_MPEG2TS = 3, OUTPUT_FORMAT_LIST_END // must be last - used to validate format type };MediaMuxer::MediaMuxer(int fd, OutputFormat format) : mFormat(format), mState(UNINITIALIZED) { ALOGV("MediaMuxer start, format=%d", format); if (format == OUTPUT_FORMAT_MPEG_4 || format == OUTPUT_FORMAT_THREE_GPP) { mWriter = new MPEG4Writer(fd); } else if (format == OUTPUT_FORMAT_WEBM) { mWriter = new WebmWriter(fd); } //add mpeg2ts else if (format == OUTPUT_FORMAT_YUNOVO_MPEG2TS){ mWriter = new MPEG2TSWriter(fd); }//add end if (mWriter != NULL) { mFileMeta = new MetaData; mState = INITIALIZED; }}
好了,到这里为止,从java到c++层的接口,就算是打通了。现在就可以使用extractor.readSampleData去抽取视频帧数据,然后使用muxer.writeSampleData去写mpeg2ts流文件了。
下面顺便说一下,这个抽帧和写帧的流程。我们在MediaMuxer.cpp里构建好Muxer后,就可以在java层上通过muxer.addTrack(format),将源文件里的视频track和音频track甚至字幕track添加进来了。
ssize_t MediaMuxer::addTrack(const sp &format) { Mutex::Autolock autoLock(mMuxerLock); if (format.get() == NULL) { ALOGE("addTrack() get a null format"); return -EINVAL; } if (mState != INITIALIZED) { ALOGE("addTrack() must be called after constructor and before start()."); return INVALID_OPERATION; } sp trackMeta = new MetaData; convertMessageToMetaData(format, trackMeta); sp newTrack = new MediaAdapter(trackMeta); status_t result = mWriter->addSource(newTrack); if (result == OK) { return mTrackList.add(newTrack); } return -1;}
我们注意到,这里的track,是一个MediaAdapter类。请大家记住这个类,因为后面我们在java层调用writeSampleData去写帧数据时,最终都是通过这个类去push buffer的。
status_t MediaMuxer::writeSampleData(const sp &buffer, size_t trackIndex, int64_t timeUs, uint32_t flags) { Mutex::Autolock autoLock(mMuxerLock); ALOGV("MediaMuxer::writeSampleData trackIndex= %zu; timeUs= %" PRIu64, trackIndex, timeUs); if (buffer.get() == NULL) { ALOGE("WriteSampleData() get an NULL buffer."); return -EINVAL; } if (mState != STARTED) { ALOGE("WriteSampleData() is called in invalid state %d", mState); return INVALID_OPERATION; } if (trackIndex >= mTrackList.size()) { ALOGE("WriteSampleData() get an invalid index %zu", trackIndex); return -EINVAL; } ALOGV("MediaMuxer::writeSampleData buffer offset = %zu, length = %zu", buffer->offset(), buffer->size()); MediaBuffer* mediaBuffer = new MediaBuffer(buffer); mediaBuffer->add_ref(); // Released in MediaAdapter::signalBufferReturned(). mediaBuffer->set_range(buffer->offset(), buffer->size()); sp sampleMetaData = mediaBuffer->meta_data(); sampleMetaData->setInt64(kKeyTime, timeUs); // Just set the kKeyDecodingTime as the presentation time for now. sampleMetaData->setInt64(kKeyDecodingTime, timeUs); if (flags & MediaCodec::BUFFER_FLAG_SYNCFRAME) { sampleMetaData->setInt32(kKeyIsSyncFrame, true); } sp currentTrack = mTrackList[trackIndex]; // This pushBuffer will wait until the mediaBuffer is consumed. return currentTrack->pushBuffer(mediaBuffer);}
每写一帧时,都会在mediaMuxer.cpp里,调用MediaAdapter的接口,去pushBuffer。这个pushBuffer,将数据push到哪里去了,可以跟到frameworks\av\media\libstagefright\MediaAdapter.cpp里来看看:
void MediaAdapter::signalBufferReturned(MediaBuffer *buffer) { Mutex::Autolock autoLock(mAdapterLock); CHECK(buffer != NULL); buffer->setObserver(0); buffer->release(); ALOGV("buffer returned %p", buffer); mBufferReturnedCond.signal();}status_t MediaAdapter::read( MediaBuffer **buffer, const ReadOptions * /* options */) { Mutex::Autolock autoLock(mAdapterLock); if (!mStarted) { ALOGV("Read before even started!"); return ERROR_END_OF_STREAM; } while (mCurrentMediaBuffer == NULL && mStarted) { ALOGV("waiting @ read()"); mBufferReadCond.wait(mAdapterLock); } if (!mStarted) { ALOGV("read interrupted after stop"); CHECK(mCurrentMediaBuffer == NULL); return ERROR_END_OF_STREAM; } CHECK(mCurrentMediaBuffer != NULL); *buffer = mCurrentMediaBuffer; mCurrentMediaBuffer = NULL; (*buffer)->setObserver(this); return OK;}status_t MediaAdapter::pushBuffer(MediaBuffer *buffer) { if (buffer == NULL) { ALOGE("pushBuffer get an NULL buffer"); return -EINVAL; } Mutex::Autolock autoLock(mAdapterLock); if (!mStarted) { ALOGE("pushBuffer called before start"); return INVALID_OPERATION; } mCurrentMediaBuffer = buffer; mBufferReadCond.signal(); ALOGV("wait for the buffer returned @ pushBuffer! %p", buffer); mBufferReturnedCond.wait(mAdapterLock); return OK;}
从pushBuffer函数里可以看到,每当mCurrentMediaBuffer = buffer;这样赋值后,就会通过mBufferReadCond.signal();发送信号。这个mBufferReadCond的接收者在read函数里。当read收到消息后,就会将值通过read的指针传送到调用read的地方。调用read的地方是frameworks\av\media\libstagefright\MPEG2TSWriter.cpp里的下面的函数:
void MPEG2TSWriter::SourceInfo::onMessageReceived(const sp &msg) { switch (msg->what()) { ...... case kWhatRead: { MediaBuffer *buffer; status_t err = mSource->read(&buffer); if (err != OK && err != INFO_FORMAT_CHANGED) { sp notify = mNotify->dup(); notify->setInt32("what", kNotifyReachedEOS); notify->setInt32("status", err); notify->post(); break; } if (err == OK) { if (mStreamType == 0x0f && mAACCodecSpecificData == NULL) { // The first audio buffer must contain CSD if not received yet. CHECK_GE(buffer->range_length(), 2u); mAACCodecSpecificData = new ABuffer(buffer->range_length()); memcpy(mAACCodecSpecificData->data(), (const uint8_t *)buffer->data() + buffer->range_offset(), buffer->range_length()); readMore(); } else if (buffer->range_length() > 0) { if (mStreamType == 0x0f) { appendAACFrames(buffer); } else { appendAVCFrame(buffer); } } else { readMore(); } buffer->release(); buffer = NULL; } // Do not read more data until told to. break; } default: TRESPASS(); }}
这里在读到数据后,通过判断是音频的还是视频的,丢给不同的函数去处理。比如是视频的话,就会丢给appendAVCFrame去处理。
void MPEG2TSWriter::SourceInfo::appendAVCFrame(MediaBuffer *buffer) { sp notify = mNotify->dup(); notify->setInt32("what", kNotifyBuffer); if (mBuffer == NULL || buffer->range_length() > mBuffer->capacity()) { mBuffer = new ABuffer(buffer->range_length()); } mBuffer->setRange(0, 0); memcpy(mBuffer->data(), (const uint8_t *)buffer->data() + buffer->range_offset(), buffer->range_length()); int64_t timeUs; CHECK(buffer->meta_data()->findInt64(kKeyTime, &timeUs)); mBuffer->meta()->setInt64("timeUs", timeUs); int32_t isSync; if (buffer->meta_data()->findInt32(kKeyIsSyncFrame, &isSync) && isSync != 0) { mBuffer->meta()->setInt32("isSync", true); } mBuffer->setRange(0, buffer->range_length()); notify->setBuffer("buffer", mBuffer); notify->post();}
从这个函数里我们可以看到,appendAVCFrame函数,只对数据帧设置时间戳和同步标志后,就通过一个通知,丢给了MPEG2TSWriter::onMessageReceived去处理。MPEG2TSWriter::onMessageReceived收到帧后的处理过程,就是最开始咱们讨论的那个地方了。
另外,如果我们是从指定的mpeg2ts流文件里抽帧,然后再通过mpeg2tswriter去打包成一个新的ts流的话,有一个地方需要注意。那就是MPEG2TSWriter::SourceInfo::appendAACFrames(MediaBuffer *buffer)这个函数里的开始的地方,加个判断:
if(mIsMuxer) { buffer->set_range(7, buffer->range_length()-7); }
因为这里加个属性来判断,当是在muxer时,就要加上下面这一行.因为现有的ts视频,每一帧音频已经加上了
7个字节的音频头.如果不将这7个字节的音频头给去掉,会导致每一帧音频上又多加了一个7字节的音频头.
这样的后果会导致大部份的播放器识别不了这个音频,播放不出了声音.
到此为止,我们就将mpeg2ts的流程梳理完成了,并且修正了录相丢帧的bug,封装了mpeg2ts muxer java层接口。
更多相关文章
- 一句话锁定MySQL数据占用元凶
- Android(安卓)Activity 与Service进行数据交互详解
- 传智播客—Android(三)数据存储之XML解析技术
- Android开发从零开始之java-数据类型
- Android中MVP模式讲解及实践
- OpenGL 实现视频编辑中的转场效果
- Android(安卓)sqlite3 数据库调试
- Android——实现全国省市区地区选择
- android v4l2 摄像头 ,不基于camera框架