Android 12(S) MultiMedia(十四)ESQueue
之前看到在ATSParser::Pogram::Stream中会创建一个ESQueue,用于存储解析出来的ES data,这个ESQueue到底是用来做什么的呢?这节就来研究研究。
1、构造函数
ESQueue的全名是ElementaryStreamQueue, 构造函数传入两个参数Mode和flags,mode指定了Stream的类型(H264、AC3等),flag用于标记是否加密加扰等信息
ElementaryStreamQueue::ElementaryStreamQueue(Mode mode, uint32_t flags) : mMode(mode), mFlags(flags), mEOSReached(false), mCASystemId(0), mAUIndex(0) { ALOGV("ElementaryStreamQueue(%p) mode %x flags %x isScrambled %d isSampleEncrypted %d", this, mode, flags, isScrambled(), isSampleEncrypted()); // Create the decryptor anyway since we don't know the use-case unless key is provided // Won't decrypt if key info not available (e.g., scanner/extractor just parsing ts files) mSampleDecryptor = isSampleEncrypted() ? #ifdef __ANDROID_APEX__ new SampleDecryptor #else new HlsSampleDecryptor #endif : NULL; }
2、appendData
parsePES之后会把获取到ES流添加到ESQueue中,这里以H264为例子看看为什么要加入到ESQueue中?
status_t err = mQueue->appendData(data, size, timeUs, payloadOffset, PES_scrambling_control);
传入参数为:ESData、数据大小、时间、负载的偏移量,PES加扰控制
status_t ElementaryStreamQueue::appendData( const void *data, size_t size, int64_t timeUs, int32_t payloadOffset, uint32_t pesScramblingControl) { if (mEOSReached) { ALOGE("appending data after EOS"); return ERROR_MALFORMED; } // 这边是一个很重要的判断,mBuffer == NULL 或 mBuffer数据为空时才会进入判断 if (!isScrambled() && (mBuffer == NULL || mBuffer->size() == 0)) { switch (mMode) { case H264: case MPEG_VIDEO: { #if 0 if (size < 4 || memcmp("\x00\x00\x00\x01", data, 4)) { return ERROR_MALFORMED; } #else uint8_t *ptr = (uint8_t *)data; // 检查NAL头,找到开始的偏移量 ssize_t startOffset = -1; for (size_t i = 0; i + 2 < size; ++i) { if (!memcmp("\x00\x00\x01", &ptr[i], 3)) { startOffset = i; break; } } if (startOffset < 0) { return ERROR_MALFORMED; } if (mFormat == NULL && startOffset > 0) { ALOGI("found something resembling an H.264/MPEG syncword " "at offset %zd", startOffset); } data = &ptr[startOffset]; size -= startOffset; #endif break; } // ...... } } // 检查是否需要扩充buffer的大小 size_t neededSize = (mBuffer == NULL ? 0 : mBuffer->size()) + size; if (mBuffer == NULL || neededSize > mBuffer->capacity()) { neededSize = (neededSize + 65535) & ~65535; ALOGV("resizing buffer to size %zu", neededSize); sp<ABuffer> buffer = new ABuffer(neededSize); if (mBuffer != NULL) { memcpy(buffer->data(), mBuffer->data(), mBuffer->size()); buffer->setRange(0, mBuffer->size()); } else { buffer->setRange(0, 0); } mBuffer = buffer; } // 将数据拷贝到mBuffer当中,并设置可读写范围 memcpy(mBuffer->data() + mBuffer->size(), data, size); mBuffer->setRange(0, mBuffer->size() + size); // 创建一个RangeInfo来保存当前buffer的长度,时间、偏移量等信息,并保存到队列当中 RangeInfo info; info.mLength = size; info.mTimestampUs = timeUs; info.mPesOffset = payloadOffset; info.mPesScramblingControl = pesScramblingControl; mRangeInfos.push_back(info); #if 0 if (mMode == AAC) { ALOGI("size = %zu, timeUs = %.2f secs", size, timeUs / 1E6); hexdump(data, size); } #endif return OK; }
这个方法的主要内容就是将ES data加入到mBuffer当中,当mBuffer为NULL或者数据为空时,会去检查NALU开始的标志,如果前三个字节为0x000001则说明到一个新的NALU,同时添加一个RangeInfo
3、dequeueAccessUnit
在上面appendData之后,还要调用dequeueAccessUnit将buffer取出来放到AnotherPacket中保存,看看dequeueAccessUnit是怎么做的?
sp<ABuffer> ElementaryStreamQueue::dequeueAccessUnit() { if (isScrambled()) { return dequeueScrambledAccessUnit(); } // 这边的flag需要根据创建ATSParser时传入的参数来确定 if ((mFlags & kFlag_AlignedData) && mMode == H264) { if (mRangeInfos.empty()) { return NULL; } RangeInfo info = *mRangeInfos.begin(); mRangeInfos.erase(mRangeInfos.begin()); sp<ABuffer> accessUnit = new ABuffer(info.mLength); memcpy(accessUnit->data(), mBuffer->data(), info.mLength); accessUnit->meta()->setInt64("timeUs", info.mTimestampUs); memmove(mBuffer->data(), mBuffer->data() + info.mLength, mBuffer->size() - info.mLength); mBuffer->setRange(0, mBuffer->size() - info.mLength); if (mFormat == NULL) { mFormat = new MetaData; if (!MakeAVCCodecSpecificData(*mFormat, accessUnit->data(), accessUnit->size())) { mFormat.clear(); } } return accessUnit; } switch (mMode) { case H264: // 调用到dequeueAccessUnitH264方法 return dequeueAccessUnitH264(); // ...... default: if (mMode != MPEG_AUDIO) { ALOGE("Unknown mode"); return NULL; } return dequeueAccessUnitMPEGAudio(); } }
MPEG2TSExtractor中创建的ATSParser传入flag参数为0,所以这里会调用到dequeueAccessUnitH264。这个方法中会有一些内容看不懂,比如NAL等,接下来会先补充一下基础知识。
VCL :vide coding layer 视频编码层。这层我的理解是做编码实现
SODB :string of data bits 原始数据比特流。由VCL产生的编码后的数据流
NAL :network abstract layer 网络抽象层。这层我理解为将编码后的结构做封装便于传输,一帧数据就是一个NAL单元
RBSP : raw byte squence playlod。将SODB进行封装成为nal_unit得到的,是一个通用封装格式,主要就是将SODB大小补充为8的倍数,补充方式为先补1,然后补0直到达到8的倍数
NALU :组成NAL的单元。将RBSP针对不同的传输网络进行重新封装之后的单元(nal_unit(RBSP)加上NAL header(1byte)) header定义了当前NALU的类型,NALU类型可以是SPS、PPS、SEI、SLICE等等
sequence:一段h264码流由多个sequence组成,一个sequence是一秒,sequence由固定的结构单元,1SPS + 1PPS + 1SEI +(I +P + B),序列中的每个单元前都有0x000001作为分割符,即每个NALU之前都有0x000001。
SPS :sequence parameter sets 序列参数集。保存了一组编码视频序列的全局参数,
PPS :picture parameter set 图像参数集。作用域编码视频序列中的一个或多个独立图像
SEI :supplemental enhancement information 附加增强信息。包括画面定时等信息
看到这些定义大概就知道了ts packet中存储的ES流应该就是一个个NALU单元,一个个NALU单元可以组成NAL,也就是一帧数据。
接下来看看dequeueAccessUnitH264干了什么
sp<ABuffer> ElementaryStreamQueue::dequeueAccessUnitH264() { const uint8_t *data = mBuffer->data(); size_t size = mBuffer->size(); Vector<NALPosition> nals; size_t totalSize = 0; size_t seiCount = 0; status_t err; const uint8_t *nalStart; size_t nalSize; bool foundSlice = false; bool foundIDR = false; ALOGV("dequeueAccessUnit_H264[%d] %p/%zu", mAUIndex, data, size); // 获取一个NALU单元的数据范围,header + SODB while ((err = getNextNALUnit(&data, &size, &nalStart, &nalSize)) == OK) { if (nalSize == 0) continue; // 1、获取NALU的类型 unsigned nalType = nalStart[0] & 0x1f; bool flush = false; // 2、NALU值 1:slice (P帧) 5:IDR 即时解码刷新(I 帧) if (nalType == 1 || nalType == 5) { if (nalType == 5) { foundIDR = true; } if (foundSlice) { //TODO: Shouldn't this have been called with nalSize-1? ABitReader br(nalStart + 1, nalSize); unsigned first_mb_in_slice = parseUE(&br); // 找到新帧时将flush置为true if (first_mb_in_slice == 0) { // This slice starts a new frame. flush = true; } } foundSlice = true; // 3、NALU值 9: 7: SPS } else if ((nalType == 9 || nalType == 7) && foundSlice) { // Access unit delimiter and SPS will be associated with the // next frame. // 找到一个SPS并且当前切片已经找到,说明当前帧已经结束了,这个SPS是属于下一序列的,flush置为true, flush = true; // 4、SEI } else if (nalType == 6 && nalSize > 0) { // found non-zero sized SEI ++seiCount; } if (flush) { // The access unit will contain all nal units up to, but excluding // the current one, separated by 0x00 0x00 0x00 0x01 startcodes. size_t auSize = 4 * nals.size() + totalSize; sp<ABuffer> accessUnit = new ABuffer(auSize); sp<ABuffer> sei; if (seiCount > 0) { sei = new ABuffer(seiCount * sizeof(NALPosition)); accessUnit->meta()->setBuffer("sei", sei); } #if !LOG_NDEBUG AString out; #endif size_t dstOffset = 0; size_t seiIndex = 0; size_t shrunkBytes = 0; for (size_t i = 0; i < nals.size(); ++i) { const NALPosition &pos = nals.itemAt(i); unsigned nalType = mBuffer->data()[pos.nalOffset] & 0x1f; if (nalType == 6 && pos.nalSize > 0) { if (seiIndex >= sei->size() / sizeof(NALPosition)) { ALOGE("Wrong seiIndex"); return NULL; } NALPosition &seiPos = ((NALPosition *)sei->data())[seiIndex++]; seiPos.nalOffset = dstOffset + 4; seiPos.nalSize = pos.nalSize; } #if !LOG_NDEBUG char tmp[128]; sprintf(tmp, "0x%02x", nalType); if (i > 0) { out.append(", "); } out.append(tmp); #endif // 拷贝分隔符 memcpy(accessUnit->data() + dstOffset, "\x00\x00\x00\x01", 4); if (mSampleDecryptor != NULL && (nalType == 1 || nalType == 5)) { uint8_t *nalData = mBuffer->data() + pos.nalOffset; size_t newSize = mSampleDecryptor->processNal(nalData, pos.nalSize); // Note: the data can shrink due to unescaping, but it can never grow if (newSize > pos.nalSize) { // don't log unless verbose, since this can get called a lot if // the caller is trying to resynchronize ALOGV("expected sample size < %u, got %zu", pos.nalSize, newSize); return NULL; } memcpy(accessUnit->data() + dstOffset + 4, nalData, newSize); dstOffset += newSize + 4; size_t thisShrunkBytes = pos.nalSize - newSize; //ALOGV("dequeueAccessUnitH264[%d]: nalType: %d -> %zu (%zu)", // nalType, (int)pos.nalSize, newSize, thisShrunkBytes); shrunkBytes += thisShrunkBytes; } else { // 拷贝数据 memcpy(accessUnit->data() + dstOffset + 4, mBuffer->data() + pos.nalOffset, pos.nalSize); dstOffset += pos.nalSize + 4; //ALOGV("dequeueAccessUnitH264 [%d] %d @%d", // nalType, (int)pos.nalSize, (int)pos.nalOffset); } } #if !LOG_NDEBUG ALOGV("accessUnit contains nal types %s", out.c_str()); #endif const NALPosition &pos = nals.itemAt(nals.size() - 1); size_t nextScan = pos.nalOffset + pos.nalSize; memmove(mBuffer->data(), mBuffer->data() + nextScan, mBuffer->size() - nextScan); mBuffer->setRange(0, mBuffer->size() - nextScan); // 取出一个timestamp int64_t timeUs = fetchTimestamp(nextScan); if (timeUs < 0LL) { ALOGE("Negative timeUs"); return NULL; } // 添加I帧标志 accessUnit->meta()->setInt64("timeUs", timeUs); if (foundIDR) { accessUnit->meta()->setInt32("isSync", 1); } // 创建MediaFormat,提取csd信息 if (mFormat == NULL) { mFormat = new MetaData; if (!MakeAVCCodecSpecificData(*mFormat, accessUnit->data(), accessUnit->size())) { mFormat.clear(); } } if (mSampleDecryptor != NULL && shrunkBytes > 0) { size_t adjustedSize = accessUnit->size() - shrunkBytes; ALOGV("dequeueAccessUnitH264[%d]: AU size adjusted %zu -> %zu", mAUIndex, accessUnit->size(), adjustedSize); accessUnit->setRange(0, adjustedSize); } ALOGV("dequeueAccessUnitH264[%d]: AU %p(%zu) dstOffset:%zu, nals:%zu, totalSize:%zu ", mAUIndex, accessUnit->data(), accessUnit->size(), dstOffset, nals.size(), totalSize); mAUIndex++; // 返回buffer return accessUnit; } NALPosition pos; pos.nalOffset = nalStart - mBuffer->data(); pos.nalSize = nalSize; // 将偏移量记录到nals当中,最后遍历取出 nals.push(pos); totalSize += nalSize; } if (err != (status_t)-EAGAIN) { ALOGE("Unexpeted err"); return NULL; } return NULL; }
这里的主要逻辑是遍历数据,查找里面的NALU单元,直到找到完整的一帧(找到下一帧的帧头说明上一帧结束),然后将一帧的数据拼接起来返回给Stream
再看ATSParser::Stream,创建AnotherPacketSource用到的meta信息就是来自于ESQueue中解析到的数据,每次加入到队列中的都是一帧数据
mSource = new AnotherPacketSource(meta); mSource->queueAccessUnit(accessUnit);
另外看看seek points,只有I帧会被用来初始化seekpoint
if (pesStartOffset >= 0 && (event != NULL) && !found && mQueue->getFormat() != NULL) { int32_t sync = 0; if (accessUnit->meta()->findInt32("isSync", &sync) && sync) { int64_t timeUs; if (accessUnit->meta()->findInt64("timeUs", &timeUs)) { found = true; event->init(pesStartOffset, mSource, timeUs, getSourceType()); } } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)