当前位置: 首页 > news >正文

ExoPlayer架构详解与源码分析(9)——TsExtractor

系列文章目录

ExoPlayer架构详解与源码分析(1)——前言
ExoPlayer架构详解与源码分析(2)——Player
ExoPlayer架构详解与源码分析(3)——Timeline
ExoPlayer架构详解与源码分析(4)——整体架构
ExoPlayer架构详解与源码分析(5)——MediaSource
ExoPlayer架构详解与源码分析(6)——MediaPeriod
ExoPlayer架构详解与源码分析(7)——SampleQueue
ExoPlayer架构详解与源码分析(8)——Loader
ExoPlayer架构详解与源码分析(9)——TsExtractor


文章目录

  • 系列文章目录
  • 前言
  • TsExtractor
  • TsDurationReader
  • SectionReader
  • PatReader
  • PmtReader
  • DefaultTsPayloadReaderFactory
  • PesReader
  • 总结


前言

上篇说完了Extractor的整体结构,本篇将详细讲解Extractor的实现,主要通过TsExtractor这个实现类来讲解,顾名思义TsExtractor是用于TS容器格式的解析器。

TS(Transport Stream,传输流)是一种封装的格式,它的全称为MPEG2-TS。

MPEG组织于1994年推出MPEG-2压缩标准,以实现视/音频服务与应用互操作的可能性,MPEG-2标准是针对标准数字电视和高清晰度电视在各种应用下的压缩方案和系统层的详细规定。在MPEG-2标准中,为了将一个或更多的音频、视频或其他的基本数据流合成单个或多个数据流,以适应于存储和传送,必须对其重新进行打包编码,在码流中还需插入各种时间标记、系统控制等信息,最后送到信道编码与调制器。这样可以形成两种数据流——传送流(TS)和节目流(PS),分别适用于不同的应用。

MPEG2-TS是一种标准数据容器格式,传输与存储音视频、节目与系统信息协议数据,主要应用于数字广播系统,譬如DVB、ATSC与IPTV。TS流是将视频、音频、PSI等数据打包成传输包进行传送。其整体的设计充分考虑了传输过程中的丢包,数据干扰等问题,特别适合用于节目传输。

科普时间结束,回归主线,看下ExoPlayer 是如何解析TS结构的

TsExtractor

在看ExoPlayer 源码前,必须先来了解下TS的整体结构,然后再结合源码,看下ExoPlayer是如何实现TS的解析的。
首先看下TS容器的结构(网图,侵删)。
(网图,侵删)
可以看到每个TS包大小为188,包含一个header和payload,这种固定块大小的结果特别适合网络传输的场景,运用的也比较多。

  • header

    名称大小(b)说明
    sync_byte8同步标记占1个字节,固定为0x47,当解析器读取到这个字节的时候就知道这是一个包开始位置
    transport_error_indicator1传输错误指示符,1’表示在相关的传输包中至少有一个不可纠正的错误位。当被置1后,在错误被纠正之前不能重置为0
    payload_unit_start_indicator1负载单元起始标示符,一个完整的数据包开始时标记为1
    transport_priority1传输优先级,0为低优先级,1为高优先级,通常取0
    pid13包的 ID,用于区分不同的包,注意这个不是唯一的,可能相同类型的包都对应同一个PID,其中PID有几个固定值用于指定类型的包,如PAT包固定值为0x0000
    transport_scrambling_control2传输加扰控制,00表示未加密
    adaptation_field_control2是否包含自适应区,‘00’保留;‘01’为无自适应域,仅含有效负载;‘10’为仅含自适应域,无有效负载;‘11’为同时带有自适应域和有效负载。
    continuity_counter4递增计数器,从0-f,起始值不一定取0,但必须是连续的,随着每一个具有相同PID的TS流分组而增加,当它达到最大值后又回复到0。范围为0~15。接收端可判断是否有包丢失及包传送顺序错误
    adaptation_field_length8自适应域长度,包含在上图的PRC分段中
    flag8取0x50表示包含PCR或0x40表示不包含PCR,包含在上图的PRC分段中
    PCR40Program Clock Reference,节目时钟参考,用于恢复出与编码端一致的系统时序时钟STC(System Time Clock)。可以理解为当前包的时间戳,时间戳一般是以90 kHz 为单位的时间戳,所以转化成正常时间戳得除以90000,这段同样包含在上图的PRC分段中
  • payload
    payload里主要包含2种类型数据PES和PSI(Program Specific Information:由对于传输流的多路分解以及节目成功再现所必要的标准数据组成)
    PSI 可以认为属于 6 个表:

    1. 节目相关表(PAT)
    2. TS 节目映射表(PMT)
    3. 网络信息表(NIT)
    4. 有条件访问表(CAT)
    5. 传输流描述表
    6. IPMP 控制信息表。

    主要介绍下下面3种

    • PAT表,包头的PID固定为0x0000,包含一个header和一个body,一个payload前3字节为header,包含了body的长度,后面则为body,主要列出所有的PMT表ID,可以通过它确定哪些PID的包是PMT表主要要来查询PMT,下面是PAT的表结构,section_length前为header,后面的属于body

      名称大小(b)说明
      table_id8PAT表固定为0x00
      section_syntax_indicator1固定为1
      zero1固定为0
      reserved2固定为11
      section_length12后面数据的长度
      transport_stream_id16传输流ID,固定为0x0001
      reserved2固定为11
      version_number5版本号,固定为00000,如果PAT有变化则版本号加1
      current_next_indicator1固定为1,表示这个PAT表可以用,如果为0则要等待下一个PAT表
      section_number8固定为0x00
      last_section_number8固定为0x00
      开始循环
      program_number16为0x0000时表示这是NIT网络信息表,节目号为0x0001时,表示这是PMT
      reserved3固定为111
      PID13PAT对应PMT的包PID值
      结束循环
      CRC3232前面数据的CRC32校验码
    • PMT表,包头的PID不固定,需要通过PAT获取,主要列出了包含的所有流类型及其对于的PID,通过它可以确定当前的包对应的是哪种流,然后针对性的解析,下面是PMT的表结构

      名称大小(b)说明
      table_id8PMT表固定为0x02
      section_syntax_indicator1固定为1
      zero1固定为0
      reserved2固定为11
      section_length12后面数据的长度
      program_number16频道号码,表示当前的PMT关联到的频道,取值0x0001
      reserved2固定为11
      version_number5版本号,固定为00000,如果PAT有变化则版本号加1
      current_next_indicator1固定为1
      section_number8固定为0x00
      last_section_number8固定为0x00
      reserved3固定为111
      PCR_PID13PCR(节目参考时钟)所在TS分组的PID,指定为视频PID
      reserved3固定为111
      program_info_length12描述信息,指定为0x000表示没有
      开始循环
      stream_type8流类型,标志是Video还是Audio还是其他数据,h.264编码对应0x1b,aac编码对应0x0f,mp3编码对应0x03
      reserved3固定为111
      elementary_PID13与stream_type对应的PID
      reserved4固定为1111
      ES_info_length12描述信息,指定为0x000表示没有
      结束循环
      CRC3232前面数据的CRC32校验码
    • PES 用于承载基本流数据的数据结构,可以理解成具体的媒体流数据,同样包含header和body,看下包结构图
      在这里插入图片描述
      PES的Header结构很复杂,这里我们说明下重要的几个

      名称大小(b)说明
      packet_start_code_prefix24固定为0x000001,同跟随它的 stream_id 一起组成标识包起始端的包起始码
      stream_id16流ID,音频取值(0xc0-0xdf),通常为0xc0视频取值(0xe0-0xef),通常为0xe0具体参照ISO/IEC 13818-1 2.4.3.7
      PES_packet_length24后面pes数据的长度,0表示长度不限制,只有视频数据长度会超过0xffff
      PTS_DTS_flags2当 PTS_DTS_flags 字段设置为‘10’时,PES 包头中 PTS 字段存在。设置为‘11’时,PES 包头中 PTS 字段和 DTS 字段均存在。设置为‘00’时,PES 包头中既无任何 PTS 字段也无任何 DTS 字段存在。值‘01’禁用
      PES_header_data_length8额外包含的数据长度,包含的PTS或者DTS数据
      PTS33presentation time stamp,显示时间戳,具体参考ISO/IEC 13818-1 2.4.3.7
      DTS33decoding time stamp,解码时间戳,具体参考ISO/IEC 13818-1 2.4.3.7

好了有了上面的知识,我们一起来看下源码是如何解析的
首先从初始化看起

public TsExtractor(@Mode int mode, @Flags int defaultTsPayloadReaderFlags, int timestampSearchBytes) {this(mode,new TimestampAdjuster(0),//创建默认的payload的解析工厂类new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags),timestampSearchBytes);}public TsExtractor(@Mode int mode,TimestampAdjuster timestampAdjuster,TsPayloadReader.Factory payloadReaderFactory,int timestampSearchBytes) {
...//初始化缓存数据大小为50个TS包大小tsPacketBuffer = new ParsableByteArray(new byte[BUFFER_SIZE], 0);
...//用于从PRC中计算时长durationReader = new TsDurationReader(timestampSearchBytes);
...resetPayloadReaders();}//初始化payload解析器private void resetPayloadReaders() {trackIds.clear();tsPayloadReaders.clear();SparseArray<TsPayloadReader> initialPayloadReaders =payloadReaderFactory.createInitialPayloadReaders();int initialPayloadReadersSize = initialPayloadReaders.size();//添加初始化默认的解析器,如果没有自定义工厂会使用DefaultTsPayloadReaderFactory此时不包含任何初始化的解析器for (int i = 0; i < initialPayloadReadersSize; i++) {tsPayloadReaders.put(initialPayloadReaders.keyAt(i), initialPayloadReaders.valueAt(i));}//添加包头PAT解析器tsPayloadReaders.put(TS_PAT_PID, new SectionReader(new PatReader()));id3Reader = null;}

看下初始化后调用的第一个方法,主要用来确定当前Extractor是否适用

  //在确定使用哪种解析器时会先调用Extractor.sniff决定当前解析器是否可以用于解析,上文中也提到了调用点@Overridepublic boolean sniff(ExtractorInput input) throws IOException {byte[] buffer = tsPacketBuffer.getData();input.peekFully(buffer, 0, TS_PACKET_SIZE * SNIFF_TS_PACKET_COUNT);//填充5*118个数据for (int startPosCandidate = 0; startPosCandidate < TS_PACKET_SIZE; startPosCandidate++) {// Try to identify at least SNIFF_TS_PACKET_COUNT packets starting with TS_SYNC_BYTE.boolean isSyncBytePatternCorrect = true;//是否有5个0x47字节连续的间隔188的数据for (int i = 0; i < SNIFF_TS_PACKET_COUNT; i++) {if (buffer[startPosCandidate + i * TS_PACKET_SIZE] != TS_SYNC_BYTE) {isSyncBytePatternCorrect = false;break;}}if (isSyncBytePatternCorrect) {input.skipFully(startPosCandidate);return true;}}return false;}

然后就开始执行主要的方法read

@Overridepublic @ReadResult int read(ExtractorInput input, PositionHolder seekPosition)throws IOException {long inputLength = input.getLength();if (tracksEnded) {//所有PMT表都解析完 tracksEnded//如果tracksEnded了,此时数据的总长度已知,且不为HLS(hls有多个ts,时长记录在m3u8文件里)boolean canReadDuration = inputLength != C.LENGTH_UNSET && mode != MODE_HLS;if (canReadDuration && !durationReader.isDurationReadFinished()) {//开始读取时长,后面会具体讲到读取方式return durationReader.readDuration(input, seekPosition, pcrPid);}//输出SeekMap表,这里保存播放时间戳和数据位置的对应关系,可以通过时间戳快速定位到数据位置//这里不深入了maybeOutputSeekMap(inputLength);//是否从头开始,这里作用是当Tarck信息解析完毕的时候会返回RESULT_SEEK//回到上面讲的外循环再次从头加载数据if (pendingSeekToStart) {pendingSeekToStart = false;seek(/* position= */ 0, /* timeUs= */ 0);if (input.getPosition() != 0) {seekPosition.position = 0;return RESULT_SEEK;}}if (tsBinarySearchSeeker != null && tsBinarySearchSeeker.isSeeking()) {return tsBinarySearchSeeker.handlePendingSeek(input, seekPosition);}}//读取至少一段Ts包if (!fillBufferWithAtLeastOnePacket(input)) {return RESULT_END_OF_INPUT;}//从上次读取位置查找第一个包的结束位置int endOfPacket = findEndOfFirstTsPacketInBuffer();int limit = tsPacketBuffer.limit();//如果超过limit,其实就是包不足188字节,这里会继续加载if (endOfPacket > limit) {return RESULT_CONTINUE;}@TsPayloadReader.Flags int packetHeaderFlags = 0;// Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.//读取4字节,也就是包头的长度int tsPacketHeader = tsPacketBuffer.readInt();if ((tsPacketHeader & 0x800000) != 0) { // 获取transport_error_indicator不等0,也就是这个包有问题// There are uncorrectable errors in this packet.tsPacketBuffer.setPosition(endOfPacket);//跳过当前包return RESULT_CONTINUE;}//获取payload_unit_start_indicator位,负载单元起始标示符,一个完整的数据包开始时标记为1packetHeaderFlags |= (tsPacketHeader & 0x400000) != 0 ? FLAG_PAYLOAD_UNIT_START_INDICATOR : 0;// Ignoring transport_priority (tsPacketHeader & 0x200000)获取包PID, &111111111111100000000取4到16位,右移8位去除后8位int pid = (tsPacketHeader & 0x1FFF00) >> 8;// Ignoring transport_scrambling_control (tsPacketHeader & 0xC0)//获取adaptation_field_control第1位,判断adaptationField是否存在boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0;//获取adaptation_field_control第2位,判断paload是否存在boolean payloadExists = (tsPacketHeader & 0x10) != 0;//由于默认只设置了PAT的解析器,所以第一次只有当PID为0时才能获取到解析器TsPayloadReader payloadReader = payloadExists ? tsPayloadReaders.get(pid) : null;if (payloadReader == null) {//不存在payload,跳过当前包tsPacketBuffer.setPosition(endOfPacket);return RESULT_CONTINUE;}// 检查连续性if (mode != MODE_HLS) {//获取continuity_counter字节int continuityCounter = tsPacketHeader & 0xF;//获取上一个计数int previousCounter = continuityCounters.get(pid, continuityCounter - 1);continuityCounters.put(pid, continuityCounter);if (previousCounter == continuityCounter) {// 相同的counter可能是重传的数据直接跳过tsPacketBuffer.setPosition(endOfPacket);return RESULT_CONTINUE;} else if (continuityCounter != ((previousCounter + 1) & 0xF)) {// 非连续性的数据,可能发生了丢包或者seek,通知解析器包不连续重置相关标记位payloadReader.seek();}}// 如果存在adaptationField跳过if (adaptationFieldExists) {//获取adaptation_field_length用于跳过相应数据int adaptationFieldLength = tsPacketBuffer.readUnsignedByte();int adaptationFieldFlags = tsPacketBuffer.readUnsignedByte();packetHeaderFlags |=(adaptationFieldFlags & 0x40) != 0 // random_access_indicator.? TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR: 0;tsPacketBuffer.skipBytes(adaptationFieldLength - 1 /* flags */);}// 开始读取payloadboolean wereTracksEnded = tracksEnded;if (shouldConsumePacketPayload(pid)) {tsPacketBuffer.setLimit(endOfPacket);//设置解析结束位置//将数据喂给相应解析器,第一次consume的解析器肯定为PAT解析器,接下来会分析payloadReader.consume(tsPacketBuffer, packetHeaderFlags);tsPacketBuffer.setLimit(limit);}//非HLS,track完成(PMT已经读取),且长度已知(非直播流)if (mode != MODE_HLS && !wereTracksEnded && tracksEnded && inputLength != C.LENGTH_UNSET) {//重新开始再读一遍,因为有可能有些媒体数据在PTM等轨道信息数据之前pendingSeekToStart = true;}tsPacketBuffer.setPosition(endOfPacket);return RESULT_CONTINUE;}

在分析PAT解析器前这里加个插曲,讲下上面说到的TsDurationReader,看下ExoPlayer是如何计算视频时长的。

TsDurationReader

直入主题readDuration

public @Extractor.ReadResult int readDuration(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) throws IOException {if (pcrPid <= 0) {return finishReadDuration(input);}if (!isLastPcrValueRead) {return readLastPcrValue(input, seekPositionHolder, pcrPid);//获取最后一个PCR的值}if (lastPcrValue == C.TIME_UNSET) {return finishReadDuration(input);}if (!isFirstPcrValueRead) {return readFirstPcrValue(input, seekPositionHolder, pcrPid);//获取最第一个PCR的值}if (firstPcrValue == C.TIME_UNSET) {return finishReadDuration(input);}long minPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(firstPcrValue);long maxPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(lastPcrValue);durationUs = maxPcrPositionUs - minPcrPositionUs;//计算差值if (durationUs < 0) {Log.w(TAG, "Invalid duration: " + durationUs + ". Using TIME_UNSET instead.");durationUs = C.TIME_UNSET;}return finishReadDuration(input);}private int readFirstPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)throws IOException {int bytesToSearch = (int) min(timestampSearchBytes, input.getLength());int searchStartPosition = 0;//从前往后依次读取包,相应的获取最后一个时就是从后往前依次读取包if (input.getPosition() != searchStartPosition) {//回到上面讲的ExtractingLoadable外部循环再次从下面指定位置打开源进行读取seekPositionHolder.position = searchStartPosition;return Extractor.RESULT_SEEK;}packetBuffer.reset(bytesToSearch);input.resetPeekPosition();input.peekFully(packetBuffer.getData(), /* offset= */ 0, bytesToSearch);firstPcrValue = readFirstPcrValueFromBuffer(packetBuffer, pcrPid);isFirstPcrValueRead = true;return Extractor.RESULT_CONTINUE;}//最终调用这个读取包头public static long readPcrFromPacket(ParsableByteArray packetBuffer, int startOfPacket, int pcrPid) {packetBuffer.setPosition(startOfPacket);if (packetBuffer.bytesLeft() < 5) {// Header = 4 bytes, adaptationFieldLength = 1 byte.return C.TIME_UNSET;}// Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.//读取包头4字节int tsPacketHeader = packetBuffer.readInt();if ((tsPacketHeader & 0x800000) != 0) {//确保包无错误// transport_error_indicator != 0 means there are uncorrectable errors in this packet.return C.TIME_UNSET;}//获取包PIDint pid = (tsPacketHeader & 0x1FFF00) >> 8;if (pid != pcrPid) {return C.TIME_UNSET;}//判断adaptationField是否存在boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0;if (!adaptationFieldExists) {return C.TIME_UNSET;}//获取adaptationField长度int adaptationFieldLength = packetBuffer.readUnsignedByte();//确认长度if (adaptationFieldLength >= 7 && packetBuffer.bytesLeft() >= 7) {int flags = packetBuffer.readUnsignedByte();//获取是否设置pcrboolean pcrFlagSet = (flags & 0x10) == 0x10;if (pcrFlagSet) {byte[] pcrBytes = new byte[6];//解析PCRpacketBuffer.readBytes(pcrBytes, /* offset= */ 0, pcrBytes.length);return readPcrValueFromPcrBytes(pcrBytes);}}return C.TIME_UNSET;}//解析PCR, & 0xFF保持原始字节数据,网络数据大端序读取,只读取了前33位,精度要求不高舍弃后7位private static long readPcrValueFromPcrBytes(byte[] pcrBytes) {return (pcrBytes[0] & 0xFFL) << 25| (pcrBytes[1] & 0xFFL) << 17| (pcrBytes[2] & 0xFFL) << 9| (pcrBytes[3] & 0xFFL) << 1| (pcrBytes[4] & 0xFFL) >> 7;}public static long ptsToUs(long pts) {return (pts * C.MICROS_PER_SECOND) / 90000;}

这里还有个插曲& 0xFF,这么做的主要原因是因为在java中byte类型为大小为1字节也就是8位,而Long整型是8字节64位,JVM在将byte转为Long时取byte作为最后1字节,其他7字节采用补码的方式填充为0xFFFFFFF,& 0xFF后就可以将前7位恢复为0x0000000保持原始的字节数据,详细可以参考byte为什么要与上0xff?这篇文章

TsDurationReader获取的时长主要通过下面几步

  1. 当流的长度已知(非直播流),从TS文件尾部查找第一个包含PCR的包的PCR值
  2. 从TS文件头部查找第一个包含PCR的包的PCR值
  3. 获取2者的差值即为时长,时间戳一般是以90 kHz 为单位再除以90000就是真实的时间戳了

好了回到主线,看下第一次的PAT解析都干了什么,由于PAT和PMT有着几乎相同的头结构,这里又抽象了一个SectionReader

SectionReader

看下公共头的解析过程

@Overridepublic void consume(ParsableByteArray data, @Flags int flags) {boolean payloadUnitStartIndicator = (flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0;int payloadStartPosition = C.INDEX_UNSET;if (payloadUnitStartIndicator) {int payloadStartOffset = data.readUnsignedByte();payloadStartPosition = data.getPosition() + payloadStartOffset;}if (waitingForPayloadStart) {if (!payloadUnitStartIndicator) {return;}waitingForPayloadStart = false;data.setPosition(payloadStartPosition);bytesRead = 0;}while (data.bytesLeft() > 0) {//还有剩余数据if (bytesRead < SECTION_HEADER_LENGTH) {//解析前3字节// Note: see ISO/IEC 13818-1, section 2.4.4.3 for detailed information on the format of// the header.if (bytesRead == 0) {int tableId = data.readUnsignedByte();//获取tableIddata.setPosition(data.getPosition() - 1);if (tableId == 0xFF /* forbidden value */) {//判断合法性// No more sections in this ts packet.waitingForPayloadStart = true;//跳过当前包return;}}int headerBytesToRead = min(data.bytesLeft(), SECTION_HEADER_LENGTH - bytesRead);// sectionData is guaranteed to have enough space because it's initialized with a 32-element// backing array and headerBytesToRead is at most 3.data.readBytes(sectionData.getData(), bytesRead, headerBytesToRead);bytesRead += headerBytesToRead;if (bytesRead == SECTION_HEADER_LENGTH) {//已将所有header数据读取到sectionDatasectionData.setPosition(0);sectionData.setLimit(SECTION_HEADER_LENGTH);sectionData.skipBytes(1); //跳过tableidint secondHeaderByte = sectionData.readUnsignedByte();//读取头第2个字节int thirdHeaderByte = sectionData.readUnsignedByte();//读取头第3个字节sectionSyntaxIndicator = (secondHeaderByte & 0x80) != 0;//获取section_syntax_indicatortotalSectionLength =//获取section_length(((secondHeaderByte & 0x0F) << 8) | thirdHeaderByte) + SECTION_HEADER_LENGTH;if (sectionData.capacity() < totalSectionLength) {//确保缓存够大能够放下body// Ensure there is enough space to keep the whole section.int limit =min(MAX_SECTION_LENGTH, max(totalSectionLength, sectionData.capacity() * 2));sectionData.ensureCapacity(limit);}}} else {// 读取bodyint bodyBytesToRead = min(data.bytesLeft(), totalSectionLength - bytesRead);// sectionData has been sized large enough for totalSectionLength when reading the header.data.readBytes(sectionData.getData(), bytesRead, bodyBytesToRead);bytesRead += bodyBytesToRead;if (bytesRead == totalSectionLength) {//已将所有body数据读取到sectionDataif (sectionSyntaxIndicator) {// This section has common syntax as defined in ISO/IEC 13818-1, section 2.4.4.11.if (Util.crc32(sectionData.getData(), 0, totalSectionLength, 0xFFFFFFFF) != 0) {//首先CRC校验数据完整性// The CRC is invalid so discard the section.waitingForPayloadStart = true;return;}sectionData.setLimit(totalSectionLength - 4); // 去除最后的32位校验位} else {// This is a private section with private defined syntax.sectionData.setLimit(totalSectionLength);}sectionData.setPosition(0);reader.consume(sectionData);//将body喂给下个解析器,如果是PAT包这里调用PAT解析器解析bytesRead = 0;}}}}

SectionReader主要做了公共头的解析,至于body则交给PatReader或者PmtReader解析

PatReader

    @Overridepublic void consume(ParsableByteArray sectionData) {int tableId = sectionData.readUnsignedByte();//PAT tableId 表固定为0x00if (tableId != 0x00 /* program_association_section */) {// See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment.return;}// section_syntax_indicator(1), '0'(1), reserved(2), section_length(4)int secondHeaderByte = sectionData.readUnsignedByte();if ((secondHeaderByte & 0x80) == 0) {// section_syntax_indicator 必须为 1. See ISO/IEC 13818-1, section 2.4.4.5.return;}// 跳过section_length(8), transport_stream_id (16), reserved (2), version_number (5),// current_next_indicator (1), section_number (8), last_section_number (8)sectionData.skipBytes(6);int programCount = sectionData.bytesLeft() / 4;//一个PMT描述为4字节,计算有多少个PMT表for (int i = 0; i < programCount; i++) {sectionData.readBytes(patScratch, 4);int programNumber = patScratch.readBits(16);//program_numberpatScratch.skipBits(3); // reserved (3)if (programNumber == 0) {//program_number==0则为NIT网络信息表,直接跳过patScratch.skipBits(13); // network_PID (13)} else {int pid = patScratch.readBits(13);if (tsPayloadReaders.get(pid) == null) {//创建PMT解析器,当下次读取到PMT的包ID时直接调用PMT解析tsPayloadReaders.put(pid, new SectionReader(new PmtReader(pid)));remainingPmts++;}}}if (mode != MODE_HLS) {tsPayloadReaders.remove(TS_PAT_PID);}}

PatReader主要工作就是将PMT表解析出来,每个PMT ID对应初始化出一个解析器,当下次读取到这些PID的包时采用对于的PmtReader

PmtReader

    @Overridepublic void consume(ParsableByteArray sectionData) {int tableId = sectionData.readUnsignedByte();//确保是PMT表if (tableId != 0x02 /* TS_program_map_section */) {// See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment.return;}// 处理时间戳TimestampAdjuster timestampAdjuster;if (mode == MODE_SINGLE_PMT || mode == MODE_HLS || remainingPmts == 1) {timestampAdjuster = timestampAdjusters.get(0);} else {timestampAdjuster =new TimestampAdjuster(timestampAdjusters.get(0).getFirstSampleTimestampUs());timestampAdjusters.add(timestampAdjuster);}// section_syntax_indicator(1), '0'(1), reserved(2), section_length(4)int secondHeaderByte = sectionData.readUnsignedByte();if ((secondHeaderByte & 0x80) == 0) {// section_syntax_indicator 必须为 1. See ISO/IEC 13818-1, section 2.4.4.9.return;}// section_length(8)sectionData.skipBytes(1);int programNumber = sectionData.readUnsignedShort();// Skip 3 bytes (24 bits), including:// reserved (2), version_number (5), current_next_indicator (1), section_number (8),// last_section_number (8)sectionData.skipBytes(3);sectionData.readBytes(pmtScratch, 2);// reserved (3), PCR_PID (13)pmtScratch.skipBits(3);pcrPid = pmtScratch.readBits(13);// Read program_info_length.sectionData.readBytes(pmtScratch, 2);pmtScratch.skipBits(4);int programInfoLength = pmtScratch.readBits(12);// Skip the descriptors.sectionData.skipBytes(programInfoLength);//初始化ID3解析器if (mode == MODE_HLS && id3Reader == null) {// Setup an ID3 track regardless of whether there's a corresponding entry, in case one// appears intermittently during playback. See [Internal: b/20261500].EsInfo id3EsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, null, Util.EMPTY_BYTE_ARRAY);id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, id3EsInfo);if (id3Reader != null) {id3Reader.init(timestampAdjuster,output,new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE));}}trackIdToReaderScratch.clear();trackIdToPidScratch.clear();int remainingEntriesLength = sectionData.bytesLeft();while (remainingEntriesLength > 0) {//开始解析PMT表数据sectionData.readBytes(pmtScratch, 5);int streamType = pmtScratch.readBits(8);pmtScratch.skipBits(3); // reservedint elementaryPid = pmtScratch.readBits(13);pmtScratch.skipBits(4); // reservedint esInfoLength = pmtScratch.readBits(12); // ES_info_length.EsInfo esInfo = readEsInfo(sectionData, esInfoLength);//读取ESInfo数据//0x05 private_sections 0x06 PES packets containing private dataif (streamType == 0x06 || streamType == 0x05) {streamType = esInfo.streamType;//使用esInfo的streamType}remainingEntriesLength -= esInfoLength + 5;int trackId = mode == MODE_HLS ? streamType : elementaryPid;if (trackIds.get(trackId)) {continue;}@NullableTsPayloadReader reader =mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3? id3Reader//根据streamType创建对应的解析器,后面会分析: payloadReaderFactory.createPayloadReader(streamType, esInfo);if (mode != MODE_HLS|| elementaryPid < trackIdToPidScratch.get(trackId, MAX_PID_PLUS_ONE)) {trackIdToPidScratch.put(trackId, elementaryPid);trackIdToReaderScratch.put(trackId, reader);//用于后续获取}}int trackIdCount = trackIdToPidScratch.size();for (int i = 0; i < trackIdCount; i++) {int trackId = trackIdToPidScratch.keyAt(i);int trackPid = trackIdToPidScratch.valueAt(i);trackIds.put(trackId, true);trackPids.put(trackPid, true);@Nullable TsPayloadReader reader = trackIdToReaderScratch.valueAt(i);if (reader != null) {if (reader != id3Reader) {//初始化所有解析器reader.init(timestampAdjuster,output,new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE));}tsPayloadReaders.put(trackPid, reader);}}if (mode == MODE_HLS) {if (!tracksEnded) {output.endTracks();remainingPmts = 0;tracksEnded = true;}} else {tsPayloadReaders.remove(pid);//解析完成移除当前PMT解析器remainingPmts = mode == MODE_SINGLE_PMT ? 0 : remainingPmts - 1;if (remainingPmts == 0) {//所以PMT表都已读取output.endTracks();//endTracks,这个时候相当于MediaPeriod的prepare过程结束,已经获取到播放媒体的相关数据tracksEnded = true;}}}

PmtReader主要作用就是获取其中的流类型,然后创建出对应的解析器,最后所以PMT初始化完成后通知上层trackEnded
那么解析器具体是如何创建的呢,这部分工作PmtReader交由payloadReaderFactory,默认实现了DefaultTsPayloadReaderFactory

DefaultTsPayloadReaderFactory

这个createPayloadReader方法里基本上将所有的流类型创建了解析器,可以当一个索引看下

  @Override@Nullablepublic TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) {switch (streamType) {case TsExtractor.TS_STREAM_TYPE_MPA:case TsExtractor.TS_STREAM_TYPE_MPA_LSF:return new PesReader(new MpegAudioReader(esInfo.language));case TsExtractor.TS_STREAM_TYPE_AAC_ADTS:return isSet(FLAG_IGNORE_AAC_STREAM)? null: new PesReader(new AdtsReader(false, esInfo.language));case TsExtractor.TS_STREAM_TYPE_AAC_LATM:return isSet(FLAG_IGNORE_AAC_STREAM)? null: new PesReader(new LatmReader(esInfo.language));case TsExtractor.TS_STREAM_TYPE_AC3:case TsExtractor.TS_STREAM_TYPE_E_AC3:return new PesReader(new Ac3Reader(esInfo.language));case TsExtractor.TS_STREAM_TYPE_AC4:return new PesReader(new Ac4Reader(esInfo.language));case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:if (!isSet(FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS)) {return null;}// Fall through.case TsExtractor.TS_STREAM_TYPE_DTS:return new PesReader(new DtsReader(esInfo.language));case TsExtractor.TS_STREAM_TYPE_H262:case TsExtractor.TS_STREAM_TYPE_DC2_H262:return new PesReader(new H262Reader(buildUserDataReader(esInfo)));case TsExtractor.TS_STREAM_TYPE_H263:return new PesReader(new H263Reader(buildUserDataReader(esInfo)));case TsExtractor.TS_STREAM_TYPE_H264:return isSet(FLAG_IGNORE_H264_STREAM)? null: new PesReader(new H264Reader(buildSeiReader(esInfo),isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES),isSet(FLAG_DETECT_ACCESS_UNITS)));case TsExtractor.TS_STREAM_TYPE_H265:return new PesReader(new H265Reader(buildSeiReader(esInfo)));case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO:return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM)? null: new SectionReader(new PassthroughSectionPayloadReader(MimeTypes.APPLICATION_SCTE35));case TsExtractor.TS_STREAM_TYPE_ID3:return new PesReader(new Id3Reader());case TsExtractor.TS_STREAM_TYPE_DVBSUBS:return new PesReader(new DvbSubtitleReader(esInfo.dvbSubtitleInfos));case TsExtractor.TS_STREAM_TYPE_AIT:return new SectionReader(new PassthroughSectionPayloadReader(MimeTypes.APPLICATION_AIT));default:return null;}}

这里关注下目前比较主流的H.264,可以看到首先是创建Pes解析器解析PES,然后从PES中解析H.264数据,组后在H.264数据中解析SEI信息

PesReader

看下PES如何解析

@Overridepublic final void consume(ParsableByteArray data, @Flags int flags) throws ParserException {Assertions.checkStateNotNull(timestampAdjuster); // Asserts init has been called.//一个状态机
...while (data.bytesLeft() > 0) {switch (state) {case STATE_FINDING_HEADER:data.skipBytes(data.bytesLeft());break;case STATE_READING_HEADER:if (continueRead(data, pesScratch.data, HEADER_SIZE)) {setState(parseHeader() ? STATE_READING_HEADER_EXTENSION : STATE_FINDING_HEADER);}break;case STATE_READING_HEADER_EXTENSION:int readLength = min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength);// Read as much of the extended header as we're interested in, and skip the rest.if (continueRead(data, pesScratch.data, readLength)&& continueRead(data, /* target= */ null, extendedHeaderLength)) {parseHeaderExtension();flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0;reader.packetStarted(timeUs, flags);setState(STATE_READING_BODY);}break;case STATE_READING_BODY:readLength = data.bytesLeft();int padding = payloadSize == C.LENGTH_UNSET ? 0 : readLength - payloadSize;if (padding > 0) {readLength -= padding;data.setLimit(data.getPosition() + readLength);}reader.consume(data);//调用下层解析器解析body,如H264Readerif (payloadSize != C.LENGTH_UNSET) {payloadSize -= readLength;if (payloadSize == 0) {reader.packetFinished();setState(STATE_READING_HEADER);}}break;default:throw new IllegalStateException();}}}//解析headerprivate boolean parseHeader() {// Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of// the header.pesScratch.setPosition(0);int startCodePrefix = pesScratch.readBits(24);if (startCodePrefix != 0x000001) {//校验合法性Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix);payloadSize = C.LENGTH_UNSET;return false;}pesScratch.skipBits(8); // stream_id.int packetLength = pesScratch.readBits(16);//获取长度pesScratch.skipBits(5); // '10' (2), PES_scrambling_control (2), PES_priority (1)dataAlignmentIndicator = pesScratch.readBit();pesScratch.skipBits(2); // copyright (1), original_or_copy (1)ptsFlag = pesScratch.readBit();//PTS_flagsdtsFlag = pesScratch.readBit();//DTS_flags// ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1),// additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1)pesScratch.skipBits(6);extendedHeaderLength = pesScratch.readBits(8);//获取额外数据长度用于DTS PTS解析
...return true;}@RequiresNonNull("timestampAdjuster")private void parseHeaderExtension() {//解析出PTS和DTS用于后续H264解析器pesScratch.setPosition(0);timeUs = C.TIME_UNSET;if (ptsFlag) {pesScratch.skipBits(4); // '0010' or '0011'long pts = (long) pesScratch.readBits(3) << 30;pesScratch.skipBits(1); // marker_bitpts |= pesScratch.readBits(15) << 15;pesScratch.skipBits(1); // marker_bitpts |= pesScratch.readBits(15);pesScratch.skipBits(1); // marker_bitif (!seenFirstDts && dtsFlag) {pesScratch.skipBits(4); // '0011'long dts = (long) pesScratch.readBits(3) << 30;pesScratch.skipBits(1); // marker_bitdts |= pesScratch.readBits(15) << 15;pesScratch.skipBits(1); // marker_bitdts |= pesScratch.readBits(15);pesScratch.skipBits(1); // marker_bittimestampAdjuster.adjustTsTimestamp(dts);seenFirstDts = true;}timeUs = timestampAdjuster.adjustTsTimestamp(pts);}}

PesReader主要是将Pes的Header解析,获取ES数据的长度,以及PES与DTS数据,然后将这些数据传递给下层ES解析器。


总结

关于TsExtractor的内容先写到这里,ES解析器可能比TS更加复杂,计划将ES解析的内容单独一篇来解析,计划以目前最为普遍的H.264格式作为分析对象,也就是对应ExoPlayer中的H264Reader。


版权声明 ©
本文为CSDN作者山雨楼原创文章
转载请注明出处
原创不易,觉得有用的话,收藏转发点赞支持

相关文章:

ExoPlayer架构详解与源码分析(9)——TsExtractor

系列文章目录 ExoPlayer架构详解与源码分析&#xff08;1&#xff09;——前言 ExoPlayer架构详解与源码分析&#xff08;2&#xff09;——Player ExoPlayer架构详解与源码分析&#xff08;3&#xff09;——Timeline ExoPlayer架构详解与源码分析&#xff08;4&#xff09;—…...

【Python 千题 —— 基础篇】输出列表方差

题目描述 题目描述 输出列表的方差。题中有一个包含数字的列表 [10, 39, 13, 48, 32, 10, 9]&#xff0c;使用 for 循环获得这个列表中所有项的方差。 输入描述 无输入。 输出描述 输出列表的方差。 示例 示例 ① 输出&#xff1a; 列表的方差是&#xff1a;228.0代码…...

【Spring总结】基于配置的方式来写Spring

本篇文章是对这两天所学的内容做一个总结&#xff0c;涵盖我这两天写的所有笔记&#xff1a; 【Spring】 Spring中的IoC&#xff08;控制反转&#xff09;【Spring】Spring中的DI&#xff08;依赖注入&#xff09;Dependence Import【Spring】bean的基础配置【Spring】bean的实…...

Unity在Windows选项下没有Auto Streaming

Unity在Windows选项下没有Auto Streaming Unity Auto Streaming插件按网上说的不太好使最终解决方案 Unity Auto Streaming插件 我用的版本是个人版免费版&#xff0c;版本号是&#xff1a;2021.2.5f1c1&#xff0c;我的里边Windows下看不到Auto Streaming选项,就像下边这张图…...

下厨房网站月度最佳栏目菜谱数据获取及分析

目录 概要 源数据获取 写Python代码爬取数据 Scala介绍与数据处理 1.Sacla介绍...

【Java 进阶篇】深入理解 JQuery 事件绑定:标准方式

在前端开发中&#xff0c;处理用户与页面的交互是至关重要的一部分。JQuery作为一个广泛应用的JavaScript库&#xff0c;为我们提供了简便而强大的事件绑定机制&#xff0c;使得我们能够更加灵活地响应用户的行为。本篇博客将深入解析 JQuery 的标准事件绑定方式&#xff0c;为…...

某app c++层3处魔改md5详解

hello everybody,本期是安卓逆向so层魔改md5教学,干货满满,可以细细品味,重点介绍的是so层魔改md5的处理. 常见的魔改md5有: 1:明文加密前处理 2:改初始化魔数 3:改k表中的值 4:改循环左移的次数 本期遇到的是124.且循环左移的次数是动态的,需要前面的加密结果处理生成 目录…...

安装MongoDB

查看MongoDB版本可以执行如下命令 mongod --version 如果是Ubuntu&#xff0c;则直接安装 sudo apt-get install -y mongodb如果是其他&#xff0c;比如Amazon Linux2。 查看Linux系统发行版类型 grep ^NAME /etc/*release 如果是 Amazon Linux 2&#xff0c;则创建一个r…...

C++加持让python程序插上翅膀——利用pybind11进行c++和python联合编程示例

目录 0、前言1、安装 pybind11库c侧python侧 2、C引入bybind11vs增加相关依赖及设置cpp中添加头文件及导出模块cpp中添加numpy相关数据结构的接收和返回编译生成dll后改成导出模块同名文件的.pyd 3、python调用c4、C引入bybind11 0、前言 在当今的计算机视觉和机器学习领域&am…...

ubuntu20.04安装cv2

查看ubuntu的版本 cat /etc/lsb-release DISTRIB_IDUbuntu DISTRIB_RELEASE20.04 DISTRIB_CODENAMEfocal DISTRIB_DESCRIPTION"Ubuntu 20.04.3 LTS"更改镜像源 cp /etc/apt/sources.list /etc/apt/sources.list.bak cat > /etc/apt/sources.listdeb http://mirr…...

Android 13.0 recovery出厂时清理中字体大小的修改

1.前言 在13.0的系统rom定制化开发中,在recovery模块也是系统中比较重要的模块,比如恢复出厂设置,recovery ota升级, 清理缓存等等,在一些1080p的设备,但是density只是240这样的设备,会在恢复出厂设置的时候,显示的字体有点小, 产品要求需要将正在清理的字体调大点,这…...

spring+pom-注意多重依赖时的兼容问题[java.lang.NoSuchMethodError]

背景&#xff1a; 项目中同时引入了依赖A和依赖B&#xff0c;而这两个依赖都依赖于项目C&#xff0c;但它们指定的C版本不一致&#xff0c;导致运行时出现了错误。 报错如&#xff1a; java.lang.NoSuchMethodError 解决方案&#xff1a; 需要在项目pom文件中引入依赖C并指定需…...

Matalab插值详解和源码

转载&#xff1a;Matalab插值详解和源码 - 知乎 (zhihu.com) 插值法 插值法又称“内插法”&#xff0c;是利用函数f (x)在某区间中已知的若干点的函数值&#xff0c;作出适当的特定函数&#xff0c;在区间的其他点上用这特定函数的值作为函数f (x)的近似值&#xff0c;这种方…...

Flask 接口

目录 前言 代码实现 简单接口实现 执行其它程序接口 携带参数访问接口 前言 有时候会想着开个一个接口来访问试试&#xff0c;这里就给出一个基础接口代码示例 代码实现 导入Flask模块&#xff0c;没安装Flask 模块需要进行 安装&#xff1a;pip install flask 使用镜…...

Vue3 toRef函数和toRefs函数

当我们在setup 中的以读取对象属性单独交出去时&#xff0c;我们会发现这样会丢失响应式&#xff1a; setup() {let person reactive({name: "张三",age: 18,job: {type: "前端",salary:10}})return {name: person.name,age: person.age,type: person.jo…...

【论文阅读】(VAE-GAN)Autoencoding beyond pixels using a learned similarity metric

论文地址;[1512.09300] Autoencoding beyond pixels using a learned similarity metric (arxiv.org) / 一、Introduction 主要讲了深度学习中生成模型存在的问题&#xff0c;即常用的相似度度量方式&#xff08;使用元素误差度量&#xff09;对于学习良好的生成模型存在一定…...

verilog之wire vs reg区别

文章目录 一、wire vs reg二、实例一、wire vs reg wire线网: 仅支持组合逻辑建模必须由assign语句赋值不能在always块中驱动用于连接子模块的输出用于定义模块的输入端口reg寄存器: 可支持组合逻辑或时序逻辑建模必须在always块中赋值二、实例 wire [7:0] cnt; assign cnt …...

力扣面试经典150题详细解析

刷题的初心 众所周知&#xff0c;算法题对于面试大厂是必不可缺的一环&#xff0c;而且对于提高逻辑思维能力有着不小的提升。所以&#xff0c;对于程序员来讲&#xff0c;无论刚入行&#xff0c;还是从业多年&#xff0c;保持一个清醒的头脑&#xff0c;具备一个良好的设计思…...

【Java 进阶篇】唤醒好运:JQuery 抽奖案例详解

在现代社交网络和电商平台中&#xff0c;抽奖活动成为吸引用户、提升用户参与度的一种常见手段。通过精心设计的抽奖页面&#xff0c;不仅可以增加用户的互动体验&#xff0c;还能在一定程度上提高品牌的知名度。本篇博客将通过详细解析 JQuery 抽奖案例&#xff0c;带领你走进…...

数据处理生产环境_利用MurmurHash3算法在Spark和Scala中生成随机颜色

需求 根据给定的轨迹编号在这一列后面生成随机颜色_16 输入数据 ("吃饭", "123"), ("吃饭", "宋江"), ("郭靖", "宋江"), ("杨过", "奥特曼"), ("周芷若", "张无忌"),…...

便利工具分享:一个proto文件的便利使用工具

最近在研究序列化&#xff0c;每次的proto文件手敲生成代码指令都很麻烦&#xff0c;干脆自己写一个泛用脚本&#xff0c;这样以后使用时候就方便了。 废话不多说&#xff0c;首先上代码&#xff1a; #!/bin/bash # 检查是否提供了文件名参数 if [ -z "$1" ]; then…...

LeetCode704.二分查找及二分法

每日一题&#xff1a;LeetCode704.二分查找 LeetCode704.二分查找知识点&#xff1a;二分法解题代码 LeetCode704.二分查找 问题描述&#xff1a;给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xff0c;写一个函数搜索 nums 中…...

2023年R1快开门式压力容器操作证模拟考试题库及R1快开门式压力容器操作理论考试试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年R1快开门式压力容器操作证模拟考试题库及R1快开门式压力容器操作理论考试试题是由安全生产模拟考试一点通提供&#xff0c;R1快开门式压力容器操作证模拟考试题库是根据R1快开门式压力容器操作最新版教材&#…...

探索NLP中的核心架构:编码器与解码器的区别

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…...

解决:Error: Missing binding xxxxx\node_modules\node-sass\vendor\win32-x64-83\

一、具体报错 二、报错原因 这个错误是由于缺少 node-sass 模块的绑定文件引起的。 三、导致原因 3.1、环境发生了变化 3.2、安装过程出现问题 四、解决方法步骤&#xff1a; 4.1、重新构建 node-sass 模块 npm rebuild node-sass 4.2、清除缓存并重新安装依赖 npm c…...

科研学习|科研软件——面板数据、截面数据、时间序列数据的区别是什么?

一、数据采集方式不同 面板数据是通过在多个时间点上对同一组体进行观测而获得的数据。面板数据可以是横向面板数据&#xff0c;即对同一时间点上不同个体的观测&#xff0c;也可以是纵向面板数据&#xff0c;即对同一个体在不同时间点上的观测。采集面板数据需要跟踪相同的个体…...

【UE5】物体沿样条线移动

目录 效果 步骤 一、使用样条线创建路径 二、创建沿样条线路径移动的物体 三、定义可移动物体的生成器 效果 步骤 一、使用样条线创建路径 先创建一个Actor蓝图&#xff0c;这里命名为“BP_Line” 该蓝图中只需添加一个样条组件 将“BP_Line”拖入场景中 按住Alt鼠标左键…...

Qt控件按钮大全

​ 按钮 在 Qt 里,最常用使用的控件就是按钮了,有了按钮,我们就可以点击,从而响应事件,达到人机交互的效果。不管是嵌入式或者 PC 端,界面交互,少不了按钮。Qt 按钮部件是一种常用的部件之一,Qt 内置了六种按钮部件如下: (1) QPushButton:下压按钮 (2) QToolBu…...

软件工程--软件过程学习笔记

本篇内容是对学校软件工程课堂内容的记录总结&#xff0c;部分也来源于网上查找的资料 软件过程基础 软件过程是指在软件开发过程中&#xff0c;经过一系列有序的步骤和活动&#xff0c;从问题定义到最终软件产品交付和维护的全过程。这个过程旨在确保软件项目能够按时、按预…...

高校教师资格证备考

高等教育制度 关于人的全面发展和个体发展的关系&#xff0c;说法正确的是&#xff08;ABC&#xff09;。 A.个体发展是在全面发展基础上的选择性发展 B.全面发展是个体发展的前提和基础 C.个体发展又是全面发展的动力 D.个体发展是全面发展的前提和基础...

Git通过rebase合并多个commit

在使用 Git 作为版本控制的时候&#xff0c;我们可能会由于各种各样的原因提交了许多临时的 commit&#xff0c;而这些 commit 拼接起来才是完整的任务。那么我们为了避免太多的 commit 而造成版本控制的混乱&#xff0c;通常我们推荐将这些 commit 合并成一个。 1. 查看提交历…...

ROS 学习应用篇(八)ROS中的坐标变换管理之tf广播与监听的编程实现

偶吼吼胜利在望&#xff0c;冲冲冲 老规矩新建功能包 工作空间目录下/src下开启终端输入 catkin_create_pkg learning_tf roscpp rospy tf turtlesim 如何实现tf广播 引入库 c python …...

计算机算法分析与设计(23)---二分搜索算法(C++)

文章目录 1. 算法介绍2. 代码编写 1. 算法介绍 1. 二分搜索&#xff08;英语&#xff1a;binary search&#xff09;&#xff0c;也称折半搜索&#xff08;英语&#xff1a;half-interval search&#xff09;、对数搜索&#xff08;英语&#xff1a;logarithmic search&#xf…...

前置语音群呼与语音机器人群呼哪个更好

最近通过观察自己接到的营销电话&#xff0c;通过语音机器人外呼的量应该有所下降。同时和客户交流获取到的信息&#xff0c;也是和这个情况类似&#xff0c;很多AI机器人群呼的量转向了OKCC前置语音群呼。询问原因&#xff0c;说是前置语音群呼转化更快&#xff0c;AI机器人群…...

『Element Plus の 百科大全』

Element Plus 官网 点击跳转...

P3879 [TJOI2010] 阅读理解- 字典树

题面 分析 将所有单词存入字典树&#xff0c;重点值怎么判断在哪一行出现过&#xff0c;对于字典树查询的判断字符串是否存在的数组可以开成二维&#xff0c;也就是在查询到某个字符串存在后&#xff0c;再通过循环判断每一层是否存在。 代码 #include <bits/stdc.h>…...

upgrade k8s (by quqi99)

作者&#xff1a;张华 发表于&#xff1a;2023-11-17 版权声明&#xff1a;可以任意转载&#xff0c;转载时请务必以超链接形式标明文章原始出处和作者信息及本版权声明(http://blog.csdn.net/quqi99) 本文只是从网上搜索一些升级k8s的理论学习&#xff0c;下面的步骤未实际测…...

CronExpression

CronTrigger配置格式: 格式: [秒] [分] [小时] [日] [月] [周] [年]序号 说明 是否必填 允许填写的值 允许的通配符 1 秒 是 0-59 , - * / 2 分 是 0-59 , - * / 3 小时 是 0-23 , - * / 4 日 是 1-31 , - * ? / L W 5 月 是 1-12 or JA…...

释放机器人潜力,INDEMIND深耕底层技术

市场转暖&#xff0c;但攘外需要同时安内。 市场降温之后&#xff0c;正迎来拐点 疫情之后&#xff0c;经济逐渐下行&#xff0c;服务机器人的“好日子”也随之结束&#xff0c;整个行业都在动荡中经历渡劫。根据TE智库报告显示&#xff0c;从2022年开始&#xff0c;我国服务…...

【ES6标准入门】JavaScript中的模块Module语法的使用细节:export命令和imprt命令详细使用,超级详细!!!

&#x1f601; 作者简介&#xff1a;一名大四的学生&#xff0c;致力学习前端开发技术 ⭐️个人主页&#xff1a;夜宵饽饽的主页 ❔ 系列专栏&#xff1a;JavaScript进阶指南 &#x1f450;学习格言&#xff1a;成功不是终点&#xff0c;失败也并非末日&#xff0c;最重要的是继…...

流量2----2

2...

人工智能发展前景

随着人工智能的快速发展&#xff0c;这个行业对人才的需求也在不断增长。越来越多的有志之士开始关注人工智能&#xff0c;希望通过自学获得相关技能&#xff0c;进而在人工智能领域找到心仪的职业。本文将探讨人工智能职业发展的前景&#xff0c;并为大家提供自学人工智能的途…...

编写程序,要求输入x的值,输出y的值。分别用(1)不嵌套的if语句(2)嵌套的if语句(3)if-else语句(4)switch语句。

编写程序&#xff0c;要求输入x的值&#xff0c;输出y的值。分别用&#xff08;1&#xff09;不嵌套的if语句&#xff08;2&#xff09;嵌套的if语句&#xff08;3&#xff09;if-else语句&#xff08;4&#xff09;switch语句。 选择结构是编程语言中常用的一种控制结构&…...

AcWing 4520:质数 ← BFS

【题目来源】https://www.acwing.com/problem/content/4523/【题目描述】 给定一个正整数 X&#xff0c;请你在 X 后面添加若干位数字&#xff08;至少添加一位数字&#xff1b;添加的数不能有前导0&#xff09;&#xff0c;使得结果为质数&#xff0c;在这个前提下所得的结果应…...

00、计算机视觉入门与调优简介

写在前面 每天更新1篇文章&#xff0c;共更新100篇以上 相关代码会放在gitee上 中间会按进度和反馈安排视频讲解 预计2023-11-11开始推送文章&#xff0c;持续3个月左右 专栏简介 本专栏带你从头开始入门计算机视觉。 内容会比之前写的文章更专业更全面&#xff0c;并且你…...

.L0CK3D来袭:如何保护您的数据免受致命攻击

尊敬的读者&#xff1a; 网络犯罪的威胁日益增长&#xff0c;其中.L0CK3D勒索病毒是一种极具挑战性的数字威胁。为了助您应对这一风险&#xff0c;本文将深入探讨.L0CK3D病毒的狡猾手法、毁灭性影响&#xff0c;提供详实的数据恢复方法&#xff0c;同时为您提供极具实战性的预…...

多媒体ffmpeg学习教程

多媒体ffmpeg 目前比较流行的音视频文件为:MP4 flv m3u8 ffmpeg ffmpeg ffplay ffprobe ffserverffmpeg -i INPUT -vf "split [main][tmp]; [tmp] cropiw:ih/2:0:0, vflip [flip];[main][flip] overlay0:H/2" OUTPUTffmpeg -i 2022.mp4 -vcodec mpeg4 -b:…...

SELinux零知识学习十五、SELinux策略语言之客体类别和许可(9)

接前一篇文章&#xff1a;SELinux零知识学习十四、SELinux策略语言之客体类别和许可&#xff08;8&#xff09; 一、SELinux策略语言之客体类别和许可 4. 客体类别许可实例 &#xff08;3&#xff09;进程客体类别许可 与文件许可不同&#xff0c;许多进程许可没有直接对应到…...

OpenSign:安全可靠的电子签名解决方案 | 开源日报 No.76

microsoft/Web-Dev-For-Beginners Stars: 71.5k License: MIT 这个开源项目是一个为期 12 周的全面课程&#xff0c;由微软云倡导者团队提供。它旨在帮助初学者掌握 JavaScript、CSS 和 HTML 的基础知识。每一节都包括预习和复习测验、详细的书面指南、解决方案、作业等内容。…...

Linux | 进程间通信

目录 前言 一、进程间通信的基本概念 二、管道 1、管道的基本概念 2、匿名管道 &#xff08;1&#xff09;原理 &#xff08;2&#xff09;测试代码 &#xff08;3&#xff09;读写控制相关问题 a、读端关闭 b、写端关闭 c、读快写慢 d、读慢些快 &#xff08;4&a…...