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());
}
}
}
Android 12(S) MultiMedia(十四)ESQueue的更多相关文章
- Android图表库MPAndroidChart(十四)——在ListView种使用相同的图表
Android图表库MPAndroidChart(十四)--在ListView种使用相同的图表 各位好久不见,最近挺忙的,所有博客更新的比较少,这里今天说个比较简单的图表,那就是在ListView中使 ...
- Gradle 1.12 翻译——第十四章. 教程 - 杂七杂八
有关其它已翻译的章节请关注Github上的项目:https://github.com/msdx/gradledoc/tree/1.12,或訪问:http://gradledoc.qiniudn.com ...
- Android UI开发第二十四篇——Action Bar
Action bar是一个标识应用程序和用户位置的窗口功能,并且给用户提供操作和导航模式.在大多数的情况下,当你需要突出展现用户行为或全局导航的activity中使用action bar,因为acti ...
- 【转】Android UI开发第二十四篇——Action Bar
Action bar是一个标识应用程序和用户位置的窗口功能,并且给用户提供操作和导航模式.在大多数的情况下,当你需要突出展现用户行为或全局导航的activity中使用action bar,因为acti ...
- Android开发(二十四)——数据存储SharePreference、SQLite、File、ContentProvider
Android提供以下四种存储方式: SharePreference SQLite File ContentProvider Android系统中数据基本都是私有的,一般存放在“data/data/程 ...
- Xamarin.Android开发实践(十四)
Xamarin.Android之ListView和Adapter 一.前言 如今不管任何应用都能够看到列表的存在,而本章我们将学习如何使用Xamarin去实现它,以及如何使用适配器和自定义适配器(本文 ...
- Android核心分析之十四Android GWES之输入系统
Android输入系统 依照惯例,在研究Android输入系统之前给出输入系统的本质描述:从哲学的观点来看,输入系统就是解决从哪里来又将到哪里去问题.输入的本质上的工作就是收集用户输入信息 ...
- 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 这 ...
- <Android 基础(三十四)> TabLayout 从头到脚
1. 简介 1.TabLayout给我们提供的是一排横向的标签页 2.#newTab()这个方法来创建新的标签页,然后用过#setText()和#setIcon方法分别修改标签页的文本和图标,创建完成 ...
- Android学习笔记(十四) Handler理论补充
一.如何下载Android源码 在SDK Manager中选中Sources for Android SDK. 二.ThreadLocal初步介绍 1)执行ThreadLocal对象(static f ...
随机推荐
- pc=mobile+pad自适应布局:页面结构与打开方式
pc=mobile+pad自适应布局 在这篇文章,咱们重点聊聊自适应布局的页面结构,以及打开页面的几种方式.关于pc=mobile+pad自适应布局的起源.概念.效果,参见文章:自适应布局:pc = ...
- Flutter笔记 - 事件分发
事件处理流程 Flutter 事件处理流程主要分两步,为了聚焦核心流程,我们以用户触摸事件为例来说明: 命中测试:当手指按下时,触发 PointerDownEvent 事件,按照深度优先遍历当前渲染( ...
- 力扣1083(MySQL)-销售分析Ⅲ(简单)
题目: Table: Product Table: Sales 编写一个SQL查询,报告2019年春季才售出的产品.即仅在2019-01-01至2019-03-31(含)之间出售的商品. 以 任意顺序 ...
- HarmonyOS NEXT应用开发之MpChart图表实现案例
介绍 MpChart是一个包含各种类型图表的图表库,主要用于业务数据汇总,例如销售数据走势图,股价走势图等场景中使用,方便开发者快速实现图表UI.本示例主要介绍如何使用三方库MpChart实现柱状图U ...
- HarmonyOS NEXT应用开发案例——二级联动
介绍 本示例主要介绍了List组件实现二级联动(Cascading List)的场景. 该场景多用于短视频中拍摄风格的选择.照片编辑时的场景的选择. 效果图预览 使用说明: 滑动二级列表侧控件,一级列 ...
- 涨姿势 | 一文读懂备受大厂青睐的ClickHouse高性能列存核心原理
简介: 本文尝试解读ClickHouse存储层的设计与实现,剖析它的性能奥妙 作者:和君 引言 ClickHouse是近年来备受关注的开源列式数据库,主要用于数据分析(OLAP)领域.目前国内各个大厂 ...
- 几种Java常用序列化框架的选型与对比
简介: 序列化与反序列化是我们日常数据持久化和网络传输中经常使用的技术,但是目前各种序列化框架让人眼花缭乱,不清楚什么场景到底采用哪种序列化框架.本文会将业界开源的序列化框架进行对比测试,分别从通用性 ...
- 使用 WPF 做个 PowerPoint 系列 文本 BodyProperties 的 FontScale 与文本字号缩放
本文来告诉大家,在 OpenXML 的 BodyProperties 的 NormalAutoFit 的 FontScale 属性缩放文本框的文本字号的方法 通过 ECMA 376 文档可以了解到 B ...
- dotnet CBB 为什么决定推送 Tag 才能打包
通过推送 Tag 才打 NuGet 包的方法的作用不仅仅是让打包方便,让打包这个动作可以完全在本地执行,无需关注其他系统的使用步骤.更重要的是可以强制每个可能被安装的 NuGet 包版本都能有一个和他 ...
- 累计预扣法个税,怎么算?(附excel)
累计预扣法个税计算 依法纳税是每个公民的义务,但看着每个月递增的个税,你可能会发出疑问,这到底是怎么算的?这就要引出2019年1月1日实施新实施的个税法,累计预扣法.即自2019年1月1日起,居民个人 ...