Android 12(S) MultiMedia(十一)从MPEG2TSExtractor到MPEG2-TS
本节主要学习内容是看看MPEG2TSExtractor是如何处理TS流的。
相关代码位置:
frameworks/av/media/extractors/mpeg2/MPEG2TSExtractor.cpp
frameworks/av/media/libstagefright/mpeg2ts/ATSParser.cpp
1、TS Header
MPEG2TSExtractor的构造函数中有个init方法用于加载ts流所包含的信息,而关键方法是feedMore,读完这个方法大致就可以直到ts包的结构以及ts流的组成,再回过头来看MPEG2TSExtractor中的方法就会很简单了。
status_t MPEG2TSExtractor::feedMore(bool isInit) {
Mutex::Autolock autoLock(mLock); uint8_t packet[kTSPacketSize];
// 1、通过DataSource读取一个ts packet
ssize_t n = mDataSource->readAt(mOffset + mHeaderSkip, packet, kTSPacketSize);
// 如果读取到的字节数小于188
if (n < (ssize_t)kTSPacketSize) {
// 通知ATSParser读取结束
if (n >= 0) {
mParser->signalEOS(ERROR_END_OF_STREAM);
}
// 如果读取错误,返回-1,否则返回EOS
return (n < 0) ? (status_t)n : ERROR_END_OF_STREAM;
}
// 利用当前的offset,创建一个同步点
ATSParser::SyncEvent event(mOffset);
// 2、更新下次读取的offset
mOffset += mHeaderSkip + n;
// 3、调用ATSParser的方法进行demux
status_t err = mParser->feedTSPacket(packet, kTSPacketSize, &event);
if (event.hasReturnedData()) {
if (isInit) {
mLastSyncEvent = event;
} else {
addSyncPoint_l(event);
}
}
return err;
}
看起来feedMore这个方法并不复杂,但是feedTSPacket这个方法实现还是挺复杂的,接下来就来看看。
status_t ATSParser::feedTSPacket(const void *data, size_t size,
SyncEvent *event) {
if (size != kTSPacketSize) {
ALOGE("Wrong TS packet size");
return BAD_VALUE;
}
// 利用读取的数据创建一个ABitReader
ABitReader br((const uint8_t *)data, kTSPacketSize);
// 真正开始demux
return parseTS(&br, event);
}
feedTSPacket中创建了个ABitReader,用它可以实现按位读取,最后就调用parseTS做真正的demux操作。下面先贴一个TS包包头格式图:
status_t ATSParser::parseTS(ABitReader *br, SyncEvent *event) {
// 1、读取同步字节 8 bit,不是0x47则丢弃
unsigned sync_byte = br->getBits(8);
if (sync_byte != 0x47u) {
ALOGE("[error] parseTS: return error as sync_byte=0x%x", sync_byte);
return BAD_VALUE;
}
// 2、读取传输误差指示符 1bit
if (br->getBits(1)) { // transport_error_indicator
// silently ignore.
return OK;
}
// 3、读取有效载荷单元起始指示符 1 bit
unsigned payload_unit_start_indicator = br->getBits(1);
// 4、读取传输优先级 1 bit
MY_LOGV("transport_priority = %u", br->getBits(1));
// 5、PID 13 bit
unsigned PID = br->getBits(13);
// 6、传输加扰控制 2 bit
unsigned transport_scrambling_control = br->getBits(2);
// 7、自适应字段控制 2 bit
unsigned adaptation_field_control = br->getBits(2);
// 8、连续计数器 4 bit
unsigned continuity_counter = br->getBits(4); status_t err = OK; // 9、判断自适应字段控制,如果是2或者3就调用parseAdaptationField
unsigned random_access_indicator = 0;
if (adaptation_field_control == 2 || adaptation_field_control == 3) {
err = parseAdaptationField(br, PID, &random_access_indicator);
}
if (err == OK) {
// 10、如果自适应字段控制为1或者3,则调用parsePID
if (adaptation_field_control == 1 || adaptation_field_control == 3) {
err = parsePID(br, PID, continuity_counter,
payload_unit_start_indicator,
transport_scrambling_control,
random_access_indicator,
event);
}
}
++mNumTSPacketsParsed;
return err;
}
上面的1 - 8步可以对应上ts包头的结构。后面用到有效载荷单元起始符payload_unit_start_indicator,和传输加扰控制。第9步会根据自适应字段控制的值去做下一步操作,所谓自适应字段控制,意思也就是它的值控制着自适应字段的内容,值与内容的对照表:
自适应字段控制值 | 对应内容 |
00 | 保留 |
01 | 没有调整字段,只含有184bytes的有效载荷 |
10 | 没有有效载荷,只含有183bytes的调整字段 |
11 | 0~182bytes的调整字段后为有效载荷 |
自适应字段控制值为2、3时有调整字段,需要去调用parseAdaptationField,
这段代码不去展开,如果PCR标志位为1需要读取后续的可选字段,后面用到的只有随机读取指示符 random_access_indicator。
到这里TS包的包头就解析结束了,接下来就要解析包中的内容了。
自适应字段控制值为1、3时有有效载荷,需要调用parsePID,这里就比较复杂了(有些内容还没搞懂),先贴代码:
status_t ATSParser::parsePID(
ABitReader *br, unsigned PID,
unsigned continuity_counter,
unsigned payload_unit_start_indicator,
unsigned transport_scrambling_control,
unsigned random_access_indicator,
SyncEvent *event) {
// 1、检查PID是否已经被记录到PSISection中
ssize_t sectionIndex = mPSISections.indexOfKey(PID);
// 2、如果已经记录
if (sectionIndex >= 0) {
sp<PSISection> section = mPSISections.valueAt(sectionIndex);
// 2.1、有效载荷单元起始符为1,说明该包承载有PSI数据的第一个字节
if (payload_unit_start_indicator) {
// 2.2、清除section中的数据
if (!section->isEmpty()) {
ALOGW("parsePID encounters payload_unit_start_indicator when section is not empty");
section->clear();
}
// 2.3、获取pointer_field,指示PSI在payload中的位置
unsigned skip = br->getBits(8);
section->setSkipBytes(skip + 1); // skip filler bytes + pointer field itself,注意这里是按字节跳过
// 2.4、跳过数据包中payload之前的数据
br->skipBits(skip * 8);
} if (br->numBitsLeft() % 8 != 0) {
return ERROR_MALFORMED;
}
// 2.5、将后续PSI分段中的数据 接到前面的PSI数据上
status_t err = section->append(br->data(), br->numBitsLeft() / 8); if (err != OK) {
return err;
}
// 2.6、检查PSI是否读取完毕
if (!section->isComplete()) {
return OK;
}
// 2.7、校验CRC
if (!section->isCRCOkay()) {
return BAD_VALUE;
}
ABitReader sectionBits(section->data(), section->size()); if (PID == 0) {
// 2.8、读取PAT表
parseProgramAssociationTable(§ionBits);
} else {
bool handled = false;
for (size_t i = 0; i < mPrograms.size(); ++i) {
status_t err;
// abc
if (!mPrograms.editItemAt(i)->parsePSISection(
PID, §ionBits, &err)) {
continue;
} if (err != OK) {
return err;
} handled = true;
break;
} if (!handled) {
mPSISections.removeItem(PID);
section.clear();
}
} if (section != NULL) {
section->clear();
} return OK;
}
// cdd
bool handled = false;
for (size_t i = 0; i < mPrograms.size(); ++i) {
status_t err;
if (mPrograms.editItemAt(i)->parsePID(
PID, continuity_counter,
payload_unit_start_indicator,
transport_scrambling_control,
random_access_indicator,
br, &err, event)) {
if (err != OK) {
return err;
} handled = true;
break;
}
} if (!handled) {
handled = mCasManager->parsePID(br, PID);
} if (!handled) {
ALOGV("PID 0x%04x not handled.", PID);
} return OK;
}
先介绍几个概念:
PSI:Program Specific Infomation 节目专用信息,意思就是保存着流中节目的信息
PAT:Program Associate Tables 节目关联表
PMT:Program Map Tables 节目映射表
进入ATSParser::parsePID这个方法之后会有两个分支:
(1)先判断当前PID是否被记录在了PSISection列表中,如果有记录则说明当前包是一个包含PSI信息的包。接下来会判断 payload_unit_start_indicator,这个值有多种意义,在包含有PSI的包中,值为1说明该包承载有PSI数据的第一个字节(换句话说PSI信息可能分成多个包发送,这个包是第一个包),如果为0则该包不是第一个包。后续的pointer_field会指示PSI数据所在的位置,但是setSkipBytes的作用不太明白,因为在向PSISection写入数据前已经skipBits了。后面append会把后续PSI的数据都组合在一起,根据不同的PID做不同的解析,下面给出PID的意义:
PID值 | TS包中负载内容 |
0x0000 | PAT |
当PID为0,说明当前PSI中包含有有个PAT,PAT节目关联表到底是什么,后面解析parseProgramAssociationTable时就知道了。
如果PID不为0,说明当前PSI中包含有个PMT,PMT节目映射表到底是什么,后面解析Program.parsePSISection时就知道了。
(2)如果PID没有被记录到PSISection中,说明当前包是一个PES包,会调用Program.parsePID解析出包含的数据
这里再记录两个概念
ES:Elementary Streams 原始流,也可以理解为编码后的数据流
PES:Packetized Elementary Streams 分组流,可以理解为将ES流分段打包之后的数据包,由包头和负载组成
2、PAT
下面就先看parseProgramAssociationTable是如何解析PAT表的:
void ATSParser::parseProgramAssociationTable(ABitReader *br) {
// 1、表id 8 bit
unsigned table_id = br->getBits(8);
if (table_id != 0x00u) {
ALOGE("PAT data error!");
return ;
}
// 2、分段句法指示符 1 bit
unsigned section_syntax_indictor = br->getBits(1);
// '0' 1 bit
br->skipBits(1); // '0'
// 保留 2 bit
MY_LOGV(" reserved = %u", br->getBits(2));
// 3、后续数据长度 12 bit
unsigned section_length = br->getBits(12);
// 4、传输流id 16 bit
MY_LOGV(" transport_stream_id = %u", br->getBits(16));
// 保留 2 bit
MY_LOGV(" reserved = %u", br->getBits(2));
// 版本信息 5 bit
MY_LOGV(" version_number = %u", br->getBits(5));
// 5、当前、下个PAT指示符
MY_LOGV(" current_next_indicator = %u", br->getBits(1));
// 6、分段号
MY_LOGV(" section_number = %u", br->getBits(8));
// 7、最后一个分段号
MY_LOGV(" last_section_number = %u", br->getBits(8));
// 计算剩余长度
size_t numProgramBytes = (section_length - 5 /* header */ - 4 /* crc */);
// 8、循环解析节目号
for (size_t i = 0; i < numProgramBytes / 4; ++i) {
// 9、获取节目号
unsigned program_number = br->getBits(16);
MY_LOGV(" reserved = %u", br->getBits(3)); if (program_number == 0) {
// 10、节目号为0,则后面跟着的是network_PID
MY_LOGV(" network_PID = 0x%04x", br->getBits(13));
} else {
// 11、节目号不为0,则后面跟着的是节目映射表PMT的id
unsigned programMapPID = br->getBits(13);
bool found = false;
// 12、循环查找PMT_id是否已经被保存
for (size_t index = 0; index < mPrograms.size(); ++index) {
const sp<Program> &program = mPrograms.itemAt(index); if (program->number() == program_number) {
// 13、如果找到则更新信息
program->updateProgramMapPID(programMapPID);
found = true;
break;
}
}
// 14、如果没找到则以节目号、PMT_id创建一个Program
if (!found) {
mPrograms.push(
new Program(this, program_number, programMapPID, mLastRecoveredPTS));
if (mSampleAesKeyItem != NULL) {
mPrograms.top()->signalNewSampleAesKey(mSampleAesKeyItem);
}
}
// 15、使用PMT_id创建一个PSISection添加到列表当中
if (mPSISections.indexOfKey(programMapPID) < 0) {
mPSISections.add(programMapPID, new PSISection);
}
}
} MY_LOGV(" CRC = 0x%08x", br->getBits(32));
}
重要步骤:
3、这里的section_length表示后续的数据长度,单位是byte,后续的5个字节都是表头(暂时无用)
8、循环解析节目号时,看到循环次数是剩余有效长度/4,因为每个节目表信息都是4byte
11、获取到节目号,节目号为0,说明后面跟着的是network_pid(作用未知);如果节目号不为零,说明后面跟着的是programMapPID
12、检查programMapPID是否被记录,如果被记录则更新Program中的id信息;如果没有记录则用节目号和programMapPID创建一个Program
15、使用programMapPID创建一个PSISection添加到列表中(从这里我们可以知道有些带有PSI信息的包的PID 其实就是节目的 programMapPID)
到这儿,大概就可以知道所谓PAT节目关联表,就是保存着program_number 和 programMapPID的表,这些表有什么用,下面就会知道了。
3、PMT
如果说带有PSI信息的包的PID不为0,说明PSI信息都是些节目信息,所以会调用Program.parsePSISection:
bool ATSParser::Program::parsePSISection(
unsigned pid, ABitReader *br, status_t *err) {
*err = OK; if (pid != mProgramMapPID) {
return false;
} *err = parseProgramMap(br); return true;
}
由于可能会有多个programMapPID,所以解析之前会先找到对应id的Program,然后调用parseProgramMap做真正的解析,这个方法是在是太长了。
status_t ATSParser::Program::parseProgramMap(ABitReader *br) {
// 1、表id 8 bit
unsigned table_id = br->getBits(8);
if (table_id != 0x02u) {
ALOGE("PMT data error!");
return ERROR_MALFORMED;
}
// 2、分段法指示符 1 bit
unsigned section_syntax_indicator = br->getBits(1);
if (section_syntax_indicator != 1u) {
ALOGE("PMT data error!");
return ERROR_MALFORMED;
}
// 0 1 bit
br->skipBits(1); // '0'
// 保留 2 bit
MY_LOGV(" reserved = %u", br->getBits(2));
// 分段长度 12 bit
unsigned section_length = br->getBits(12);
// 3、节目号 16 bit
MY_LOGV(" program_number = %u", br->getBits(16));
// 保留 2 bit
MY_LOGV(" reserved = %u", br->getBits(2));
bool audioPresentationsChanged = false;
// 版本号 5 bit
unsigned pmtVersion = br->getBits(5);
if (pmtVersion != mPMTVersion) {
audioPresentationsChanged = true;
mPMTVersion = pmtVersion;
}
// 当前下一个指示符 1 bit
MY_LOGV(" current_next_indicator = %u", br->getBits(1));
// 分段序列号 8 bit
MY_LOGV(" section_number = %u", br->getBits(8));
// 最后一段编号 8 bit
MY_LOGV(" last_section_number = %u", br->getBits(8));
// 保留 3 bit
MY_LOGV(" reserved = %u", br->getBits(3));
// PCR_PIC 13 bit
unsigned PCR_PID = br->getBits(13);
// 保留 4 bit
MY_LOGV(" reserved = %u", br->getBits(4));
// 4、CADescriptor信息长度 12 bit
unsigned program_info_length = br->getBits(12); // descriptors
CADescriptor programCA;
// 5、这里比较奇怪并没有找到相关资料
bool hasProgramCA = findCADescriptor(br, program_info_length, &programCA); if (hasProgramCA && !mParser->mCasManager->addProgram(
mProgramNumber, programCA)) {
return ERROR_MALFORMED;
} Vector<StreamInfo> infos;
// 计算剩余数据长度
int32_t infoBytesRemaining = section_length - 9 - program_info_length - 4;
// 剩余数据长度必须大于5,因为每个信息块的长度至少是5个字节
while (infoBytesRemaining >= 5) {
StreamInfo info;
// 6、流类型 8 bit
info.mType = br->getBits(8);
// 保留 3 bit
MY_LOGV(" reserved = %u", br->getBits(3));
// 7、流id 13 bit
info.mPID = br->getBits(13);
// 保留
MY_LOGV(" reserved = %u", br->getBits(4));
// ES信息长度 12 bit
unsigned ES_info_length = br->getBits(12);
infoBytesRemaining -= 5 + ES_info_length; // 如果ES_info_length,需要减去改长度 CADescriptor streamCA;
// 8、扩展类型
info.mTypeExt = EXT_DESCRIPTOR_DVB_RESERVED_MAX; info.mAudioPresentations.clear();
bool hasStreamCA = false;
// 确认ES信息长度大于2byte,以及剩余长度大于0(说明有足够空间存储ES信息)
while (ES_info_length > 2 && infoBytesRemaining >= 0) {
// 9、描述标签 8 bit
unsigned descriptor_tag = br->getBits(8);
// 10、描述信息长度 8 bit
unsigned descriptor_length = br->getBits(8); ES_info_length -= 2;
if (descriptor_length > ES_info_length) {
return ERROR_MALFORMED;
}
// 11、接下就不太清楚在做什么了
if (descriptor_tag == DESCRIPTOR_CA && descriptor_length >= 4) {
hasStreamCA = true;
streamCA.mSystemID = br->getBits(16);
streamCA.mPID = br->getBits(16) & 0x1fff;
ES_info_length -= descriptor_length;
descriptor_length -= 4;
streamCA.mPrivateData.assign(br->data(), br->data() + descriptor_length);
br->skipBits(descriptor_length * 8);
} else if (info.mType == STREAMTYPE_PES_PRIVATE_DATA &&
descriptor_tag == DESCRIPTOR_DVB_EXTENSION && descriptor_length >= 1) {
unsigned descTagExt = br->getBits(8);
ALOGV(" tag_ext = 0x%02x", descTagExt);
ES_info_length -= descriptor_length;
descriptor_length--;
// The AC4 descriptor is used in the PSI PMT to identify streams which carry AC4
// audio.
if (descTagExt == EXT_DESCRIPTOR_DVB_AC4) {
info.mTypeExt = EXT_DESCRIPTOR_DVB_AC4;
br->skipBits(descriptor_length * 8);
} else if (descTagExt == EXT_DESCRIPTOR_DVB_AUDIO_PRESELECTION &&
descriptor_length >= 1) {
// DVB BlueBook A038 Table 110
unsigned num_preselections = br->getBits(5);
br->skipBits(3); // reserved
for (unsigned i = 0; i < num_preselections; ++i) {
if (br->numBitsLeft() < 16) {
ALOGE("Not enough data left in bitreader!");
return ERROR_MALFORMED;
}
AudioPresentationV1 ap;
ap.mPresentationId = br->getBits(5); // preselection_id // audio_rendering_indication
ap.mMasteringIndication = static_cast<MasteringIndication>(br->getBits(3));
ap.mAudioDescriptionAvailable = (br->getBits(1) == 1);
ap.mSpokenSubtitlesAvailable = (br->getBits(1) == 1);
ap.mDialogueEnhancementAvailable = (br->getBits(1) == 1); bool interactivity_enabled = (br->getBits(1) == 1);
MY_LOGV(" interactivity_enabled = %d", interactivity_enabled); bool language_code_present = (br->getBits(1) == 1);
bool text_label_present = (br->getBits(1) == 1); bool multi_stream_info_present = (br->getBits(1) == 1);
bool future_extension = (br->getBits(1) == 1);
if (language_code_present) {
if (br->numBitsLeft() < 24) {
ALOGE("Not enough data left in bitreader!");
return ERROR_MALFORMED;
}
char language[4];
language[0] = br->getBits(8);
language[1] = br->getBits(8);
language[2] = br->getBits(8);
language[3] = 0;
ap.mLanguage = String8(language);
} // This maps the presentation id to the message id in the
// EXT_DESCRIPTOR_DVB_MESSAGE so that we can get the presentation label.
if (text_label_present) {
if (br->numBitsLeft() < 8) {
ALOGE("Not enough data left in bitreader!");
return ERROR_MALFORMED;
}
unsigned message_id = br->getBits(8);
MY_LOGV(" message_id = %u", message_id);
} if (multi_stream_info_present) {
if (br->numBitsLeft() < 8) {
ALOGE("Not enough data left in bitreader!");
return ERROR_MALFORMED;
}
unsigned num_aux_components = br->getBits(3);
br->skipBits(5); // reserved
if (br->numBitsLeft() < (num_aux_components * 8)) {
ALOGE("Not enough data left in bitreader!");
return ERROR_MALFORMED;
}
br->skipBits(num_aux_components * 8); // component_tag
}
if (future_extension) {
if (br->numBitsLeft() < 8) {
return ERROR_MALFORMED;
}
br->skipBits(3); // reserved
unsigned future_extension_length = br->getBits(5);
if (br->numBitsLeft() < (future_extension_length * 8)) {
ALOGE("Not enough data left in bitreader!");
return ERROR_MALFORMED;
}
br->skipBits(future_extension_length * 8); // future_extension_byte
}
info.mAudioPresentations.push_back(std::move(ap));
}
} else {
br->skipBits(descriptor_length * 8);
}
} else {
ES_info_length -= descriptor_length;
br->skipBits(descriptor_length * 8);
}
}
if (hasStreamCA && !mParser->mCasManager->addStream(
mProgramNumber, info.mPID, streamCA)) {
return ERROR_MALFORMED;
}
if (hasProgramCA) {
info.mCADescriptor = programCA;
} else if (hasStreamCA) {
info.mCADescriptor = streamCA;
}
// 将所有的info加入到列表中
infos.push(info);
} if (infoBytesRemaining != 0) {
ALOGW("Section data remains unconsumed");
}
unsigned crc = br->getBits(32);
if (crc != mPMT_CRC) {
audioPresentationsChanged = true;
mPMT_CRC = crc;
} bool PIDsChanged = false;
// 12、检查对应ID流的类型是否有改变
for (size_t i = 0; i < infos.size(); ++i) {
StreamInfo &info = infos.editItemAt(i); ssize_t index = mStreams.indexOfKey(info.mPID); if (index >= 0 && mStreams.editValueAt(index)->type() != info.mType) {
ALOGI("uh oh. stream PIDs have changed.");
PIDsChanged = true;
break;
}
} if (PIDsChanged) {
// we can recover if number of streams for each type remain the same
bool success = switchPIDs(infos); if (!success) {
ALOGI("Stream PIDs changed and we cannot recover.");
return ERROR_MALFORMED;
}
} bool isAddingScrambledStream = false;
// 13、检查流是否被创建
for (size_t i = 0; i < infos.size(); ++i) {
StreamInfo &info = infos.editItemAt(i); if (mParser->mCasManager->isCAPid(info.mPID)) {
// skip CA streams (EMM/ECM)
continue;
}
ssize_t index = mStreams.indexOfKey(info.mPID); if (index < 0) {
// 14、使用PCR_PID 和 StreamInfo创建一个新流
sp<Stream> stream = new Stream(this, PCR_PID, info); if (mSampleAesKeyItem != NULL) {
stream->signalNewSampleAesKey(mSampleAesKeyItem);
} isAddingScrambledStream |= info.mCADescriptor.mSystemID >= 0;
// 15、将新流以流id为key加入到列表
mStreams.add(info.mPID, stream);
}
else if (index >= 0 && mStreams.editValueAt(index)->isAudio()
&& audioPresentationsChanged) {
mStreams.editValueAt(index)->setAudioPresentations(info.mAudioPresentations);
}
} if (isAddingScrambledStream) {
return ERROR_DRM_DECRYPT_UNIT_NOT_INITIALIZED;
}
return OK;
}
看几个步骤:
5、CADescrpitor暂时还不明白是做什么用的
6、从这一步开始可以提取出有用的信息,打包成是StreamInfo,这一步可以解析出流的类型
7、解析出流的ID
8、mTypeExt 意思好像是扩展类型,比如音频的dobly等
9、descriptor_tag意思可能是扩展类型的标签,根据该标签来确定mTypeExt
12、检查已经保存的流的类型 和 新解析出来的对应id的流的类型是否一致
13、检查对应ID的流有没有被创建,如果没有则以Program、PCR_PID、StreamInfo创建一个Stream,并添加到mStream列表中管理
可以看到上面给的PMT信息表 和 代码解析上有很大的差距,一个是在CADescriptor处的解析上有不同,另一个是在mTypeExt的解析上有不同。
到这儿就知道了PMT节目映射表中保存的是流id以及流的信息比如streamType,以及对应的扩展类型,用解析出的StreamInfo就可以创建出对应的Stream了
4、PES
前面解析的包都是包含PSI信息的包,PID为0时解析的是PAT表,PID为programMapID时解析出的是PMT表,剩下一种PID为StreamID的情况还没看,接下来就来看看。
bool handled = false;
for (size_t i = 0; i < mPrograms.size(); ++i) {
status_t err;
if (mPrograms.editItemAt(i)->parsePID(
PID, continuity_counter,
payload_unit_start_indicator,
transport_scrambling_control,
random_access_indicator,
br, &err, event)) {
if (err != OK) {
return err;
} handled = true;
break;
}
} bool ATSParser::Program::parsePID(
unsigned pid, unsigned continuity_counter,
unsigned payload_unit_start_indicator,
unsigned transport_scrambling_control,
unsigned random_access_indicator,
ABitReader *br, status_t *err, SyncEvent *event) {
*err = OK; ssize_t index = mStreams.indexOfKey(pid);
if (index < 0) {
return false;
} *err = mStreams.editValueAt(index)->parse(
continuity_counter,
payload_unit_start_indicator,
transport_scrambling_control,
random_access_indicator,
br, event); return true;
}
PID为StreamID时会遍历所有的Program调用其parsePID方法,本质上时查找Program中所有的Stream的id,如果匹配成功则调用对应Stream的parse方法,做真正的解析PES的动作。
status_t ATSParser::Stream::parse(
unsigned continuity_counter,
unsigned payload_unit_start_indicator,
unsigned transport_scrambling_control,
unsigned random_access_indicator,
ABitReader *br, SyncEvent *event) {
if (mQueue == NULL) {
return OK;
}
// 1、检查包中的连续计数 和 预期的连续计数是否相同
if (mExpectedContinuityCounter >= 0
&& (unsigned)mExpectedContinuityCounter != continuity_counter) {
ALOGI("discontinuity on stream pid 0x%04x", mElementaryPID); mPayloadStarted = false;
mPesStartOffsets.clear();
mBuffer->setRange(0, 0);
mSubSamples.clear();
mExpectedContinuityCounter = -1;
// 负载开始指示符为0说明包中无PES数据
if (!payload_unit_start_indicator) {
return OK;
}
}
// 预期计数加1
mExpectedContinuityCounter = (continuity_counter + 1) & 0x0f;
// 负载开始指示符为1说明有数据,开始解析数据
if (payload_unit_start_indicator) {
off64_t offset = (event != NULL) ? event->getOffset() : 0;
// 第一笔数据到来时不会进入到这个判断当中
if (mPayloadStarted) {
// 2、处理数据
status_t err = flush(event); if (err != OK) {
ALOGW("Error (%08x) happened while flushing; we simply discard "
"the PES packet and continue.", err);
}
}
// 第一笔数据到来时会将负载开始置true
mPayloadStarted = true;
// There should be at most 2 elements in |mPesStartOffsets|.
while (mPesStartOffsets.size() >= 2) {
mPesStartOffsets.erase(mPesStartOffsets.begin());
}
mPesStartOffsets.push_back(offset);
} if (!mPayloadStarted) {
return OK;
} size_t payloadSizeBits = br->numBitsLeft();
if (payloadSizeBits % 8 != 0u) {
ALOGE("Wrong value");
return BAD_VALUE;
} size_t neededSize = mBuffer->size() + payloadSizeBits / 8;
if (!ensureBufferCapacity(neededSize)) {
return NO_MEMORY;
}
// 3、拷贝包中的数据到mBuffer中
memcpy(mBuffer->data() + mBuffer->size(), br->data(), payloadSizeBits / 8);
// 设置mBuffer的读写范围
mBuffer->setRange(0, mBuffer->size() + payloadSizeBits / 8); if (mScrambled) {
mSubSamples.push_back({payloadSizeBits / 8,
transport_scrambling_control, random_access_indicator});
} return OK;
}
这里还要先看下 payload_unit_start_indicator 负载开始指示符,之前在PSI包中碰到过,现在PES包中也有这个指示符,当然指代的意思肯定是不一样的。PES包中负载开始指示符为1说明后面的有效负载将从第一个字节开始,为0就没有负载数据。continuity_counter应该就是包中的计数器。接下来看看几个步骤:
1、如果预期计数和实际计数不相同则说明当前数据包不连续
2、第一笔数据到来时并不会进入到flush中去处理数据,因为此时mBuffer中并没有数据,flush方法处理的是上一笔的数据
3、将新送来的包中的PES数据拷贝到mBuffer当中,等待下次调用flush处理
看到这里就知道了,PES数据的解析还要调用flush方法:
status_t ATSParser::Stream::flush(SyncEvent *event) {
if (mBuffer == NULL || mBuffer->size() == 0) {
return OK;
} ALOGV("flushing stream 0x%04x size = %zu", mElementaryPID, mBuffer->size()); status_t err = OK;
if (mScrambled) {
err = flushScrambled(event);
mSubSamples.clear();
} else {
ABitReader br(mBuffer->data(), mBuffer->size());
err = parsePES(&br, event);
} mBuffer->setRange(0, 0); return err;
}
flush方法很简单,如果是普通数据就调用parsePES,如果是加扰数据就调用flushScrambled方法。我们这里只看parsePES:
先看下PES包的结构:
status_t ATSParser::Stream::parsePES(ABitReader *br, SyncEvent *event) {
const uint8_t *basePtr = br->data();
// 1、PES开始码 24 bit,固定为1
unsigned packet_startcode_prefix = br->getBits(24); if (packet_startcode_prefix != 1) {
ALOGV("Supposedly payload_unit_start=1 unit does not start "
"with startcode."); return ERROR_MALFORMED;
}
// 2、流ID 8 bit
unsigned stream_id = br->getBits(8);
// 3、PES包长度 16 bit
unsigned PES_packet_length = br->getBits(16);
// 从这里大概可以知道stream_id有一些固定的取值
if (stream_id != 0xbc // program_stream_map
&& stream_id != 0xbe // padding_stream
&& stream_id != 0xbf // private_stream_2
&& stream_id != 0xf0 // ECM
&& stream_id != 0xf1 // EMM
&& stream_id != 0xff // program_stream_directory
&& stream_id != 0xf2 // DSMCC
&& stream_id != 0xf8) { // H.222.1 type E
// 4、'10' 2 bit
if (br->getBits(2) != 2u) {
return ERROR_MALFORMED;
}
// 5、PES加扰控制 2 bit
unsigned PES_scrambling_control = br->getBits(2);
// 6、PES优先级 1 bit
MY_LOGV("PES_priority = %u", br->getBits(1));
// 7、数据对齐指示符 1 bit
MY_LOGV("data_alignment_indicator = %u", br->getBits(1));
// 8、版权 1 bit
MY_LOGV("copyright = %u", br->getBits(1));
// 9、原始或拷贝 1 bit
MY_LOGV("original_or_copy = %u", br->getBits(1));
// 10、PTS和DTS标志位 2 bit
unsigned PTS_DTS_flags = br->getBits(2);
// 11、ESCR标志位 1 bit
unsigned ESCR_flag = br->getBits(1);
// 12、
unsigned ES_rate_flag = br->getBits(1);
// 13、
unsigned DSM_trick_mode_flag = br->getBits(1);
ALOGV("DSM_trick_mode_flag = %u", DSM_trick_mode_flag);
// 14、
unsigned additional_copy_info_flag = br->getBits(1);
ALOGV("additional_copy_info_flag = %u", additional_copy_info_flag);
// 15、PES CRC校验
MY_LOGV("PES_CRC_flag = %u", br->getBits(1));
MY_LOGV("PES_extension_flag = %u", br->getBits(1));
// 16、PES数据长度
unsigned PES_header_data_length = br->getBits(8);
ALOGV("PES_header_data_length = %u", PES_header_data_length); unsigned optional_bytes_remaining = PES_header_data_length; uint64_t PTS = 0, DTS = 0;
// 17 、解析PTS/DTS 5 bytes
if (PTS_DTS_flags == 2 || PTS_DTS_flags == 3) {
// 数据至少5byte
if (optional_bytes_remaining < 5u) {
return ERROR_MALFORMED;
} if (br->getBits(4) != PTS_DTS_flags) {
return ERROR_MALFORMED;
}
PTS = ((uint64_t)br->getBits(3)) << 30;
if (br->getBits(1) != 1u) {
return ERROR_MALFORMED;
}
PTS |= ((uint64_t)br->getBits(15)) << 15;
if (br->getBits(1) != 1u) {
return ERROR_MALFORMED;
}
PTS |= br->getBits(15);
if (br->getBits(1) != 1u) {
return ERROR_MALFORMED;
} ALOGV("PTS = 0x%016" PRIx64 " (%.2f)", PTS, PTS / 90000.0); optional_bytes_remaining -= 5; if (PTS_DTS_flags == 3) {
if (optional_bytes_remaining < 5u) {
return ERROR_MALFORMED;
} if (br->getBits(4) != 1u) {
return ERROR_MALFORMED;
} DTS = ((uint64_t)br->getBits(3)) << 30;
if (br->getBits(1) != 1u) {
return ERROR_MALFORMED;
}
DTS |= ((uint64_t)br->getBits(15)) << 15;
if (br->getBits(1) != 1u) {
return ERROR_MALFORMED;
}
DTS |= br->getBits(15);
if (br->getBits(1) != 1u) {
return ERROR_MALFORMED;
} ALOGV("DTS = %" PRIu64, DTS); optional_bytes_remaining -= 5;
}
}
// 18、解析ESCR 6bytes
if (ESCR_flag) {
if (optional_bytes_remaining < 6u) {
return ERROR_MALFORMED;
} br->getBits(2); uint64_t ESCR = ((uint64_t)br->getBits(3)) << 30;
if (br->getBits(1) != 1u) {
return ERROR_MALFORMED;
}
ESCR |= ((uint64_t)br->getBits(15)) << 15;
if (br->getBits(1) != 1u) {
return ERROR_MALFORMED;
}
ESCR |= br->getBits(15);
if (br->getBits(1) != 1u) {
return ERROR_MALFORMED;
} ALOGV("ESCR = %" PRIu64, ESCR);
MY_LOGV("ESCR_extension = %u", br->getBits(9)); if (br->getBits(1) != 1u) {
return ERROR_MALFORMED;
} optional_bytes_remaining -= 6;
}
// 19、解析ES rate,3bytes
if (ES_rate_flag) {
if (optional_bytes_remaining < 3u) {
return ERROR_MALFORMED;
} if (br->getBits(1) != 1u) {
return ERROR_MALFORMED;
}
MY_LOGV("ES_rate = %u", br->getBits(22));
if (br->getBits(1) != 1u) {
return ERROR_MALFORMED;
} optional_bytes_remaining -= 3;
} br->skipBits(optional_bytes_remaining * 8);
// 20、到这里信息全部解析结束,下面就是ES数据了
// ES data follows.
int32_t pesOffset = br->data() - basePtr; if (PES_packet_length != 0) {
// 21、校验数据长度
if (PES_packet_length < PES_header_data_length + 3) {
return ERROR_MALFORMED;
} unsigned dataLength =
PES_packet_length - 3 - PES_header_data_length; if (br->numBitsLeft() < dataLength * 8) {
ALOGE("PES packet does not carry enough data to contain "
"payload. (numBitsLeft = %zu, required = %u)",
br->numBitsLeft(), dataLength * 8); return ERROR_MALFORMED;
} ALOGV("There's %u bytes of payload, PES_packet_length=%u, offset=%d",
dataLength, PES_packet_length, pesOffset);
// 22、调用onPayloadData处理负载ES数据
onPayloadData(
PTS_DTS_flags, PTS, DTS, PES_scrambling_control,
br->data(), dataLength, pesOffset, event); br->skipBits(dataLength * 8);
} else {
onPayloadData(
PTS_DTS_flags, PTS, DTS, PES_scrambling_control,
br->data(), br->numBitsLeft() / 8, pesOffset, event); size_t payloadSizeBits = br->numBitsLeft();
if (payloadSizeBits % 8 != 0u) {
return ERROR_MALFORMED;
} ALOGV("There's %zu bytes of payload, offset=%d",
payloadSizeBits / 8, pesOffset);
}
} else if (stream_id == 0xbe) { // padding_stream
if (PES_packet_length == 0u) {
return ERROR_MALFORMED;
}
br->skipBits(PES_packet_length * 8);
} else {
if (PES_packet_length == 0u) {
return ERROR_MALFORMED;
}
br->skipBits(PES_packet_length * 8);
} return OK;
}
这里来看看parsePES中的一些步骤:
2、解析出stream_id,这个stream_id并不是用于创建流的id
3、PES_packet_length 解析出的是PES包后续的总长度
5~15、解析PES包头中的flags
16、PES_header_data_length 解析出的是PES头数据长度,这个头可能包含有四部分PTS/DTS、ESCR、ES_rate、其他,其他包含的是其余flag对应的数据,由于没有用到所以统一划分到其他分类
17~19、分别为PTS/DTS、ESCR、ES_rate的解析过程
21、校验数据长度,PES包的长度应该大于等于标志位的长度 + PES头数据的长度,ES数据等于PES包长度 - 标志位长度(3bytes) - PES头数据长度(标志位对应的数据的长度)
22、剩余的数据就是ES数据了,调用onPayloadData来处理数据
接下来就看看onPayloadData是怎么处理ES数据的吧:
void ATSParser::Stream::onPayloadData(
unsigned PTS_DTS_flags, uint64_t PTS, uint64_t /* DTS */,
unsigned PES_scrambling_control,
const uint8_t *data, size_t size,
int32_t payloadOffset, SyncEvent *event) { ALOGV("onPayloadData mStreamType=0x%02x size: %zu", mStreamType, size); int64_t timeUs = 0LL; // no presentation timestamp available.
if (PTS_DTS_flags == 2 || PTS_DTS_flags == 3) {
// 1、将PTS转化为时间戳
timeUs = mProgram->convertPTSToTimestamp(PTS);
}
// 2、将数据追加到ESQueue当中
status_t err = mQueue->appendData(
data, size, timeUs, payloadOffset, PES_scrambling_control); if (mEOSReached) {
mQueue->signalEOS();
} if (err != OK) {
return;
} sp<ABuffer> accessUnit;
bool found = false;
// 3、从ESQueue中出取出一个buffer
while ((accessUnit = mQueue->dequeueAccessUnit()) != NULL) {
// 4、如果AnotherPacketSource为空则创建一个,并把format传给它
if (mSource == NULL) {
sp<MetaData> meta = mQueue->getFormat(); if (meta != NULL) {
ALOGV("Stream PID 0x%08x of type 0x%02x now has data.",
mElementaryPID, mStreamType); const char *mime;
if (meta->findCString(kKeyMIMEType, &mime)
&& !strcasecmp(mime, MEDIA_MIMETYPE_VIDEO_AVC)) {
int32_t sync = 0;
if (!accessUnit->meta()->findInt32("isSync", &sync) || !sync) {
continue;
}
}
mSource = new AnotherPacketSource(meta);
if (mAudioPresentations.size() > 0) {
addAudioPresentations(accessUnit);
}
// 5、将取出的buffer保存到AnotherPacketSource中
mSource->queueAccessUnit(accessUnit);
ALOGV("onPayloadData: created AnotherPacketSource PID 0x%08x of type 0x%02x",
mElementaryPID, mStreamType);
}
} else if (mQueue->getFormat() != NULL) {
// After a discontinuity we invalidate the queue's format
// and won't enqueue any access units to the source until
// the queue has reestablished the new format. if (mSource->getFormat() == NULL) {
mSource->setFormat(mQueue->getFormat());
}
if (mAudioPresentations.size() > 0) {
addAudioPresentations(accessUnit);
}
mSource->queueAccessUnit(accessUnit);
} // Every access unit has a pesStartOffset queued in |mPesStartOffsets|.
// 6、从Offset队列中取出一个值
off64_t pesStartOffset = -1;
if (!mPesStartOffsets.empty()) {
pesStartOffset = *mPesStartOffsets.begin();
mPesStartOffsets.erase(mPesStartOffsets.begin());
} 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;
// 7、初始化syncPoint
event->init(pesStartOffset, mSource, timeUs, getSourceType());
}
}
}
}
}
这个方法的关键步骤不是很多:
2、将ES数据全部写入到ESQueue中,ESQueue是什么?下面会简单了解以下
3、ES数据经过ESQueue处理之后会再被取出来,用AnotherPacketSouce来保存;如果Source还没有创建,则创建一个并且用ESQueue中保存的Format初始化它
6、在ATSParser::Stream::parse中预先将文件偏移量offset保存在mPesStartOffsets当中,这里取出来,协同时间戳一起初始化一个SyncPoint,以后就可以通过时间戳找到对应的文件偏移量了。这里我还有点个人理解:不是每个ts packet都有PTS信息的,所以并不是每次都会去创建SyncPoint,只有对应偏移量的ts packet中有PTS信息时才可以用于seek,第7步上面的两个if判断大概就是做此工作。
下面看看ESQueue是做什么的:
ESQueue的创建最早出现在Stream的构造函数ATSParser::Stream::Stream中。
ATSParser::Stream::Stream(
Program *program, unsigned PCR_PID, const StreamInfo &info)
: mProgram(program),
mElementaryPID(info.mPID),
mStreamType(info.mType), // 流类型
mStreamTypeExt(info.mTypeExt), // 流额外类型
mPCR_PID(PCR_PID),
mExpectedContinuityCounter(-1),
mPayloadStarted(false),
mEOSReached(false),
mPrevPTS(0),
mQueue(NULL),
mScrambled(info.mCADescriptor.mSystemID >= 0),
mAudioPresentations(info.mAudioPresentations) {
mSampleEncrypted =
mStreamType == STREAMTYPE_H264_ENCRYPTED ||
mStreamType == STREAMTYPE_AAC_ENCRYPTED ||
mStreamType == STREAMTYPE_AC3_ENCRYPTED; ALOGV("new stream PID 0x%02x, type 0x%02x, scrambled %d, SampleEncrypted: %d",
info.mPID, info.mType, mScrambled, mSampleEncrypted); uint32_t flags = 0;
// 1、生成flags
if (((isVideo() || isAudio()) && mScrambled)) {
flags = ElementaryStreamQueue::kFlag_ScrambledData;
} else if (mSampleEncrypted) {
flags = ElementaryStreamQueue::kFlag_SampleEncryptedData;
}
// 2、初始化mode
ElementaryStreamQueue::Mode mode = ElementaryStreamQueue::INVALID;
// 3、根据流类型来生成对应的flags和mode
switch (mStreamType) {
case STREAMTYPE_H264:
case STREAMTYPE_H264_ENCRYPTED:
mode = ElementaryStreamQueue::H264;
flags |= (mProgram->parserFlags() & ALIGNED_VIDEO_DATA) ?
ElementaryStreamQueue::kFlag_AlignedData : 0;
break; case STREAMTYPE_MPEG2_AUDIO_ADTS:
case STREAMTYPE_AAC_ENCRYPTED:
mode = ElementaryStreamQueue::AAC;
break; case STREAMTYPE_MPEG1_AUDIO:
case STREAMTYPE_MPEG2_AUDIO:
mode = ElementaryStreamQueue::MPEG_AUDIO;
break; case STREAMTYPE_MPEG1_VIDEO:
case STREAMTYPE_MPEG2_VIDEO:
mode = ElementaryStreamQueue::MPEG_VIDEO;
break; case STREAMTYPE_MPEG4_VIDEO:
mode = ElementaryStreamQueue::MPEG4_VIDEO;
break; case STREAMTYPE_LPCM_AC3:
case STREAMTYPE_AC3:
case STREAMTYPE_AC3_ENCRYPTED:
mode = ElementaryStreamQueue::AC3;
break; case STREAMTYPE_EAC3:
mode = ElementaryStreamQueue::EAC3;
break; case STREAMTYPE_PES_PRIVATE_DATA:
if (mStreamTypeExt == EXT_DESCRIPTOR_DVB_AC4) {
mode = ElementaryStreamQueue::AC4;
}
break; case STREAMTYPE_METADATA:
mode = ElementaryStreamQueue::METADATA;
break; default:
ALOGE("stream PID 0x%02x has invalid stream type 0x%02x",
info.mPID, info.mType);
return;
}
// 4、利用flags和mode创建ESQueue
mQueue = new ElementaryStreamQueue(mode, flags); if (mQueue != NULL) {
if (mSampleAesKeyItem != NULL) {
mQueue->signalNewSampleAesKey(mSampleAesKeyItem);
} ensureBufferCapacity(kInitialStreamBufferSize);
// 5、如果是加扰数据则预先初始化format
if (mScrambled && (isAudio() || isVideo())) {
// Set initial format to scrambled
sp<MetaData> meta = new MetaData();
meta->setCString(kKeyMIMEType,
isAudio() ? MEDIA_MIMETYPE_AUDIO_SCRAMBLED
: MEDIA_MIMETYPE_VIDEO_SCRAMBLED);
// for MediaExtractor.CasInfo
const CADescriptor &descriptor = info.mCADescriptor;
meta->setInt32(kKeyCASystemID, descriptor.mSystemID); meta->setData(kKeyCAPrivateData, 0,
descriptor.mPrivateData.data(),
descriptor.mPrivateData.size()); mSource = new AnotherPacketSource(meta);
}
}
}
这里比较重要的是mStreamType 和 mStreamTypeExt,他们会影响isAudio和isVideo的返回结果以及 flags 和mode,最终创建的ESQueue也会有所不同。
appendData这里就不作展开了,主要是对编码格式不太了解,appendData大概是在对传入的ES数据做格式加工,保存到mData中;dequeueAcessUnit方法会根据不同StreamType将数据划分为不同的unit返回给Stream,这里暂时不做过多的了解。
到这儿,解析TS包的工作就做完了!!!
了解了如何使用ATSParser去解析TS packet,那么MPEG2TSExtractor中的内容就很容易理解了,暂时就先写道这里。
Android 12(S) MultiMedia(十一)从MPEG2TSExtractor到MPEG2-TS的更多相关文章
- Android图表库MPAndroidChart(十一)——多层级的堆叠条形图
Android图表库MPAndroidChart(十一)--多层级的堆叠条形图 事实上这个也是条形图的一种扩展,我们看下效果就知道了 是吧,他一般满足的需求就是同类数据比较了,不过目前我还真没看过哪个 ...
- Android特效专辑(十一)——仿水波纹流量球进度条控制器,实现高端大气的主流特效
Android特效专辑(十一)--仿水波纹流球进度条控制器,实现高端大气的主流特效 今天看到一个效果挺不错的,就模仿了下来,加上了一些自己想要的效果,感觉还不错的样子,所以就分享出来了,话不多说,上图 ...
- Android 12(S) 图形显示系统 - 示例应用(二)
1 前言 为了更深刻的理解Android图形系统抽象的概念和BufferQueue的工作机制,这篇文章我们将从Native Level入手,基于Android图形系统API写作一个简单的图形处理小程序 ...
- Android 12(S) 图形显示系统 - 基本概念(一)
1 前言 Android图形系统是系统框架中一个非常重要的子系统,与其它子系统一样,Android 框架提供了各种用于 2D 和 3D 图形渲染的 API供开发者使用来创建绚丽多彩的应用APP.图形渲 ...
- Android 12(S) 图形显示系统 - 应用建立和SurfaceFlinger的沟通桥梁(三)
1 前言 上一篇文章中我们已经创建了一个Native示例应用,从使用者的角度了解了图形显示系统API的基本使用,从这篇文章开始我们将基于这个示例应用深入图形显示系统API的内部实现逻辑,分析运作流程. ...
- Android 12(S) 图形显示系统 - SurfaceFlinger的启动和消息队列处理机制(四)
1 前言 SurfaceFlinger作为Android图形显示系统处理逻辑的核心单元,我们有必要去了解其是如何启动,初始化及进行消息处理的.这篇文章我们就来简单分析SurfaceFlinger这个B ...
- Android 12(S) 图形显示系统 - createSurface的流程(五)
题外话 刚刚开始着笔写作这篇文章时,正好看电视在采访一位92岁的考古学家,在他的日记中有这样一句话,写在这里与君共勉"不要等待幸运的降临,要去努力的掌握知识".如此朴实的一句话,此 ...
- Android 12(S) 图形显示系统 - BufferQueue/BLASTBufferQueue之初识(六)
题外话 你有没有听见,心里有一声咆哮,那一声咆哮,它好像在说:我就是要从后面追上去! 写文章真的好痛苦,特别是自己对这方面的知识也一知半解就更加痛苦了.这已经是这个系列的第六篇了,很多次都想放弃了,但 ...
- Android 12(S) 图形显示系统 - 初识ANativeWindow/Surface/SurfaceControl(七)
题外话 "行百里者半九十",是说步行一百里路,走过九十里,只能算是走了一半.因为步行越接近目的地,走起来越困难.借指凡事到了接近成功,往往是最吃力.最艰难的时段.劝人做事贵在坚持, ...
- Android 12(S) 图形显示系统 - BufferQueue的工作流程(八)
题外话 最近总有一个感觉:在不断学习中,越发的感觉自己的无知,自己是不是要从"愚昧之巅"掉到"绝望之谷"了,哈哈哈 邓宁-克鲁格效应 一.前言 前面的文章中已经 ...
随机推荐
- ddddocr基本使用和介绍
ddddocr基本使用和介绍 摘要:在使用爬虫登录网站的时候,经常输入用户名和密码后会遇到验证码,这时候就需要用到今天给大家介绍的python第三方库ddddocr,ddddocr是一款强大的通用开源 ...
- stmp 501 5.1.3 Invalid Address 无效的邮件地址
stmp 501 5.1.3 Invalid Address 无效的邮件地址 一般来说就是要确认邮箱地址是不是对的 还有一种可能的情况是使用的邮件服务器仅支持对内邮件,没有对外邮件的发送权限
- 剑指offer51(Java)-数组中的逆序对(困难)
题目: 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对.输入一个数组,求出这个数组中的逆序对的总数. 示例1: 输入: [7,5,6,4] 输出: 5 限制: 0 &l ...
- 重磅 | 数据库自治服务DAS论文入选全球顶会SIGMOD,领航“数据库自动驾驶”新时代
简介: 近日,智能数据库和DAS团队研发的智能调参ResTune系统论文被SIGMOD 2021录用,SIGMOD是数据库三大顶会之首,是三大顶会中唯一一个Double Blind Review的,其 ...
- WPF 字体 FontStyle 的 Italic 和 Oblique 的区别
本文介绍在 WPF 里面的字体属性 FontStyle 的 Italic 和 Oblique 的斜体差别 本文的图片和知识来自: #265 – Specifying Values for FontSt ...
- CF620E New Year Tree (线段树维护 dfs 序)
CF620E New Year Tree 题意:给出一棵 n 个节点的树,根节点为 1.每个节点上有一种颜色 ci.m 次操作.操作有两种: 1 u c:将以 u 为根的子树上的所有节点的颜色改为 ...
- Windows下绑定线程到指定的CPU核心
在某些场景下,需要把程序绑定到指定CPU核心提高执行效率.通过微软官方文档查询到Windows提供了两个Win32函数:SetThreadAffinityMask和SetProcessAffinity ...
- 我的 Kafka 旅程 - 概念 · 特点 · 组成 · 模式 · 应用
系列目录 我的 Kafka 旅程 - 概念 · 特点 · 组成 · 模式 · 应用 我的 Kafka 旅程 - Linux下的安装 · 基础命令 · 集群 我的 Kafka 旅程 - Producer ...
- DNS(1) -- DNS服务及dns资源类型
目录 1.1 DNS服务概述 1.2 DNS域名结构 1.3 DNS解析原理 1.3.1 DNS查询类型 1.3.2 解析答案 1.4 DNS资源记录类型 1.1 DNS服务概述 DNS(Domain ...
- Maven - cmd命令行窗口创建maven项目
一.构建命令 mvn archetype:generate 当出现以上的命令提示,直接回车下一步即可: 二.输入maven项目的groupId.artifactId.version 三.maven项目 ...