当前位置: 首页 > 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"), ("吃饭", "宋江"), ("郭靖", "宋江"), ("杨过", "奥特曼"), ("周芷若", "张无忌"),…...

Chapter03-Authentication vulnerabilities

文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...

Leetcode 3576. Transform Array to All Equal Elements

Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接&#xff1a;3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到&#xf…...

【Go】3、Go语言进阶与依赖管理

前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课&#xff0c;做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程&#xff0c;它的核心机制是 Goroutine 协程、Channel 通道&#xff0c;并基于CSP&#xff08;Communicating Sequential Processes&#xff0…...

精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南

精益数据分析&#xff08;97/126&#xff09;&#xff1a;邮件营销与用户参与度的关键指标优化指南 在数字化营销时代&#xff0c;邮件列表效度、用户参与度和网站性能等指标往往决定着创业公司的增长成败。今天&#xff0c;我们将深入解析邮件打开率、网站可用性、页面参与时…...

Mac下Android Studio扫描根目录卡死问题记录

环境信息 操作系统: macOS 15.5 (Apple M2芯片)Android Studio版本: Meerkat Feature Drop | 2024.3.2 Patch 1 (Build #AI-243.26053.27.2432.13536105, 2025年5月22日构建) 问题现象 在项目开发过程中&#xff0c;提示一个依赖外部头文件的cpp源文件需要同步&#xff0c;点…...

【从零学习JVM|第三篇】类的生命周期(高频面试题)

前言&#xff1a; 在Java编程中&#xff0c;类的生命周期是指类从被加载到内存中开始&#xff0c;到被卸载出内存为止的整个过程。了解类的生命周期对于理解Java程序的运行机制以及性能优化非常重要。本文会深入探寻类的生命周期&#xff0c;让读者对此有深刻印象。 目录 ​…...

从面试角度回答Android中ContentProvider启动原理

Android中ContentProvider原理的面试角度解析&#xff0c;分为​​已启动​​和​​未启动​​两种场景&#xff1a; 一、ContentProvider已启动的情况 1. ​​核心流程​​ ​​触发条件​​&#xff1a;当其他组件&#xff08;如Activity、Service&#xff09;通过ContentR…...

stm32wle5 lpuart DMA数据不接收

配置波特率9600时&#xff0c;需要使用外部低速晶振...

高分辨率图像合成归一化流扩展

大家读完觉得有帮助记得关注和点赞&#xff01;&#xff01;&#xff01; 1 摘要 我们提出了STARFlow&#xff0c;一种基于归一化流的可扩展生成模型&#xff0c;它在高分辨率图像合成方面取得了强大的性能。STARFlow的主要构建块是Transformer自回归流&#xff08;TARFlow&am…...

向量几何的二元性:叉乘模长与内积投影的深层联系

在数学与物理的空间世界中&#xff0c;向量运算构成了理解几何结构的基石。叉乘&#xff08;外积&#xff09;与点积&#xff08;内积&#xff09;作为向量代数的两大支柱&#xff0c;表面上呈现出截然不同的几何意义与代数形式&#xff0c;却在深层次上揭示了向量间相互作用的…...