之前看到在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 附加增强信息。包括画面定时等信息

参考:NAL单元简介 NAL单元详解 NAL语法结构

看到这些定义大概就知道了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());
}
}
}

Android 12(S) MultiMedia(十四)ESQueue的更多相关文章

  1. Android图表库MPAndroidChart(十四)——在ListView种使用相同的图表

    Android图表库MPAndroidChart(十四)--在ListView种使用相同的图表 各位好久不见,最近挺忙的,所有博客更新的比较少,这里今天说个比较简单的图表,那就是在ListView中使 ...

  2. Gradle 1.12 翻译——第十四章. 教程 - 杂七杂八

    有关其它已翻译的章节请关注Github上的项目:https://github.com/msdx/gradledoc/tree/1.12,或訪问:http://gradledoc.qiniudn.com ...

  3. Android UI开发第二十四篇——Action Bar

    Action bar是一个标识应用程序和用户位置的窗口功能,并且给用户提供操作和导航模式.在大多数的情况下,当你需要突出展现用户行为或全局导航的activity中使用action bar,因为acti ...

  4. 【转】Android UI开发第二十四篇——Action Bar

    Action bar是一个标识应用程序和用户位置的窗口功能,并且给用户提供操作和导航模式.在大多数的情况下,当你需要突出展现用户行为或全局导航的activity中使用action bar,因为acti ...

  5. Android开发(二十四)——数据存储SharePreference、SQLite、File、ContentProvider

    Android提供以下四种存储方式: SharePreference SQLite File ContentProvider Android系统中数据基本都是私有的,一般存放在“data/data/程 ...

  6. Xamarin.Android开发实践(十四)

    Xamarin.Android之ListView和Adapter 一.前言 如今不管任何应用都能够看到列表的存在,而本章我们将学习如何使用Xamarin去实现它,以及如何使用适配器和自定义适配器(本文 ...

  7. Android核心分析之十四Android GWES之输入系统

          Android输入系统 依照惯例,在研究Android输入系统之前给出输入系统的本质描述:从哲学的观点来看,输入系统就是解决从哪里来又将到哪里去问题.输入的本质上的工作就是收集用户输入信息 ...

  8. Android OpenGL ES(十四)gl10方法解析

    Android 支持 OpenGL 列表 1.GL 2.GL 10 3.GL 10 EXT 4.GL 11 5.GL 11 EXT 6.GL 11 ExtensionPack 我们将使用 GL10 这 ...

  9. <Android 基础(三十四)> TabLayout 从头到脚

    1. 简介 1.TabLayout给我们提供的是一排横向的标签页 2.#newTab()这个方法来创建新的标签页,然后用过#setText()和#setIcon方法分别修改标签页的文本和图标,创建完成 ...

  10. Android学习笔记(十四) Handler理论补充

    一.如何下载Android源码 在SDK Manager中选中Sources for Android SDK. 二.ThreadLocal初步介绍 1)执行ThreadLocal对象(static f ...

随机推荐

  1. 「译文」深入了解Kubernetes和Nomad

    ️原文链接: https://www.cncf.io/blog/2023/10/23/introduction-a-closer-look-at-kubernetes-and-nomad/ ✍️作者: ...

  2. 《深入理解Java虚拟机》读书笔记:运行时栈帧结构

    代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步. 一.概述 在Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型成为各种虚拟机执行引擎的统 ...

  3. HDC2021技术分论坛:HarmonyOS本地模拟器重磅来袭!

    作者:longjiangyun,模拟器开发工程师 HarmonyOS模拟器是应用开发者使用IDE进行代码开发.调试.测试等活动中必不可少的工具,它分为本地模拟器和远程模拟器,其中远程模拟器又分为单设备 ...

  4. http协议重新整理——————历史[一]

    前言 简单整理一些http协议. 正文 20 世纪 60 年代,美国国防部高等研究计划署(ARPA)建立了 ARPA 网,它有四个分布在各地的节点,被认为是如今互联网的"始祖". ...

  5. etcd 集群安装

    1.环境准备 下载安装包:https://github.com/etcd-io/etcd/releases/ 这里下载的安装包为:etcd-v3.5.9-linux-amd64.tar.gz,即我们当 ...

  6. 第 9章 数据分析案例:Python 岗位行情

    第 9章 数据分析案例:Python 岗位行情 9.1 数据爬取 (1)打开某招聘网站首页 https://www.lagou.com,选择"全国站",在搜索栏输入 Python, ...

  7. 对话 Dubbo 唤醒者北纬:3.0 将至,阿里核心电商业务也在用 Dubbo

    简介: 如今,Dubbo 已经毕业一年,越来越多开发者开始询问 Dubbo 3.0 到底有哪些变化,阿里巴巴内部到底用不用 Dubbo,这是不是一个 KPI 开源项目以及 Dubbo 和 Spring ...

  8. MAE 自监督算法介绍和基于 EasyCV 的复现

    ​简介:自监督学习(Self-Supervised Learning)能利用大量无标注的数据进行表征学习,然后在特定下游任务上对参数进行微调.通过这样的方式,能够在较少有标注数据上取得优于有监督学习方 ...

  9. 深信服智能边缘计算平台与 OpenYurt 落地方案探索与实践

    ​简介:本文将介绍边缘计算落地的机遇与挑战,以及边缘容器开源项目 OpenYurt 在企业生产环境下的实践方案. 作者:赵震,深信服云计算开发工程师,OpenYurt 社区 Member 编者案:在 ...

  10. [Nova] KeyValue Field 设置默认 key 的方式

    1. 使用 withMeta: KeyValue::make('options') ->withMeta([ 'value' => $this->options ?? [ 'A' = ...