写点什么

ExoPlayer 播放在线 TS 文件无声音问题分析

作者:Changing Lin
  • 2021 年 11 月 26 日
  • 本文字数:5208 字

    阅读完需:约 17 分钟

1.现象

  • 后台流媒体服务具有录制的功能,音频编码是 opus,视频编码是 h264,容器是 mpeg.ts

  • 需要在 App 上面支持在线播放 TS 文件的功能,经过预研,选择 Google 官方的 ExoPlayer

  • 经过一番的集成后,编译正常,播放也正常,但是播放时只有画面没有声音

2.分析

  • 因为容器是 ts,查看 ExoPlayer 源码,把关注点放到了

com.google.android.exoplayer2.extractor.ts.TsExtractor
复制代码
  • 添加日志打印

@Override  public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition)      throws IOException {    long inputLength = input.getLength();    if (tracksEnded) {      boolean canReadDuration = inputLength != C.LENGTH_UNSET && mode != MODE_HLS;      if (canReadDuration && !durationReader.isDurationReadFinished()) {        return durationReader.readDuration(input, seekPosition, pcrPid);      }      maybeOutputSeekMap(inputLength);
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); } }
if (!fillBufferWithAtLeastOnePacket(input)) { return RESULT_END_OF_INPUT; }
int endOfPacket = findEndOfFirstTsPacketInBuffer(); int limit = tsPacketBuffer.limit(); 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. int tsPacketHeader = tsPacketBuffer.readInt(); if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator // There are uncorrectable errors in this packet. tsPacketBuffer.setPosition(endOfPacket); return RESULT_CONTINUE; } packetHeaderFlags |= (tsPacketHeader & 0x400000) != 0 ? FLAG_PAYLOAD_UNIT_START_INDICATOR : 0; // Ignoring transport_priority (tsPacketHeader & 0x200000) int pid = (tsPacketHeader & 0x1FFF00) >> 8; // Ignoring transport_scrambling_control (tsPacketHeader & 0xC0) boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0; boolean payloadExists = (tsPacketHeader & 0x10) != 0; TsPayloadReader payloadReader = payloadExists ? tsPayloadReaders.get(pid) : null; Log.e("cclin", String .format("read: %d-%d-%d-%s-%s====%s", tsPacketHeader, packetHeaderFlags, pid, adaptationFieldExists, payloadExists, payloadReader));
if (payloadReader == null) { tsPacketBuffer.setPosition(endOfPacket); return RESULT_CONTINUE; }
// Discontinuity check. if (mode != MODE_HLS) { int continuityCounter = tsPacketHeader & 0xF; int previousCounter = continuityCounters.get(pid, continuityCounter - 1); continuityCounters.put(pid, continuityCounter);
Log.e("cclin", String.format("read: %d-", continuityCounter));
if (previousCounter == continuityCounter) { // Duplicate packet found. tsPacketBuffer.setPosition(endOfPacket); return RESULT_CONTINUE; } else if (continuityCounter != ((previousCounter + 1) & 0xF)) { // Discontinuity found. payloadReader.seek(); } }
// Skip the adaptation field. if (adaptationFieldExists) { 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 */); }
// Read the payload. boolean wereTracksEnded = tracksEnded; if (shouldConsumePacketPayload(pid)) { tsPacketBuffer.setLimit(endOfPacket); payloadReader.consume(tsPacketBuffer, packetHeaderFlags); tsPacketBuffer.setLimit(limit); } if (mode != MODE_HLS && !wereTracksEnded && tracksEnded && inputLength != C.LENGTH_UNSET) { // We have read all tracks from all PMTs in this non-live stream. Now seek to the beginning // and read again to make sure we output all media, including any contained in packets prior // to those containing the track information. pendingSeekToStart = true; }
tsPacketBuffer.setPosition(endOfPacket); return RESULT_CONTINUE; }
private void resetPayloadReaders() { trackIds.clear(); tsPayloadReaders.clear(); SparseArray<TsPayloadReader> initialPayloadReaders = payloadReaderFactory.createInitialPayloadReaders(); int initialPayloadReadersSize = initialPayloadReaders.size(); for (int i = 0; i < initialPayloadReadersSize; i++) { Log.e("cclin", String.format("resetPayloadReaders: %d-%s", initialPayloadReaders.keyAt(i), initialPayloadReaders.valueAt(i))); tsPayloadReaders.put(initialPayloadReaders.keyAt(i), initialPayloadReaders.valueAt(i)); } tsPayloadReaders.put(TS_PAT_PID, new SectionReader(new PatReader())); id3Reader = null; }
private class PatReader implements SectionPayloadReader {
private final ParsableBitArray patScratch;
public PatReader() { patScratch = new ParsableBitArray(new byte[4]); }
@Override public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { // Do nothing. }
@Override public void consume(ParsableByteArray sectionData) { int tableId = sectionData.readUnsignedByte(); if (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 must be 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 = 4;// int programCount = sectionData.readShort(); Log.e("cclin", String.format("consume: programCount = %d", programCount)); for (int i = 0; i < programCount; i++) { sectionData.readBytes(patScratch, 4); int programNumber = patScratch.readBits(16); patScratch.skipBits(3); // reserved (3) if (programNumber == 0) { patScratch.skipBits(13); // network_PID (13) } else { int pid = patScratch.readBits(13); Log.e("cclin", String.format("consume: xxxxxxxxx %d-%s", pid, tsPayloadReaders.get(pid))); if (tsPayloadReaders.get(pid) == null) { tsPayloadReaders.put(pid, new SectionReader(new PmtReader(pid))); remainingPmts++; } } } if (mode != MODE_HLS) { tsPayloadReaders.remove(TS_PAT_PID); } }
}
复制代码

3.日志

E/cclin: read: 1195376656-1-0-false-true====com.google.android.exoplayer2.extractor.ts.SectionReader@ee24d57E/cclin: read: 0-E/cclin: consume: programCount = 1E/cclin: consume: xxxxxxxxx 4095-nullE/cclin: 构造PmtReader: 4095E/cclin: read: 1196424976-1-4095-false-true====com.google.android.exoplayer2.extractor.ts.SectionReader@3235a044E/cclin: read: 0-E/cclin: 解析pmt的streamType: 27--1E/cclin: 解析pmt的streamType: 6--1E/cclin: read: 1195376656-1-0-false-true====nullE/cclin: read: 1196424976-1-4095-false-true====nullE/cclin: read: 1195442480-1-257-true-true====nullE/cclin: read: 1195376657-1-0-false-true====nullE/cclin: read: 1196424977-1-4095-false-true====nullE/cclin: read: 1195442481-1-257-true-true====nullE/cclin: read: 1195376658-1-0-false-true====nullE/cclin: read: 1196424978-1-4095-false-true====nullE/cclin: read: 1195442482-1-257-true-true====nullE/cclin: read: 1195442483-1-257-true-true====nullE/cclin: read: 1195442484-1-257-true-true====nullE/cclin: read: 1195442485-1-257-true-true====nullE/cclin: read: 1195442486-1-257-true-true====nullE/cclin: read: 1195442487-1-257-true-true====nullE/cclin: read: 1195442488-1-257-true-true====nullE/cclin: read: 1195442489-1-257-true-true====nullE/cclin: read: 1195442490-1-257-true-true====nullE/cclin: read: 1195442491-1-257-true-true====nullE/cclin: read: 1195376659-1-0-false-true====nullE/cclin: read: 1196424979-1-4095-false-true====nullE/cclin: read: 1195442492-1-257-true-true====nullE/cclin: read: 1195442493-1-257-true-true====nullE/cclin: read: 1195442494-1-257-true-true====nullE/cclin: read: 1195376660-1-0-false-true====nullE/cclin: read: 1196424980-1-4095-false-true====nullE/cclin: read: 1195442224-1-256-true-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62E/cclin: read: 0-E/cclin: 读取到 224E/cclin: read: 1191247889-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62E/cclin: read: 1-E/cclin: read: 1191247890-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62E/cclin: read: 2-E/cclin: read: 1191247891-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62E/cclin: read: 3-E/cclin: read: 1191247892-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62E/cclin: read: 4-E/cclin: read: 1191247893-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62E/cclin: read: 5-E/cclin: read: 1191247894-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62E/cclin: read: 6-E/cclin: read: 1191247895-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62E/cclin: read: 7-E/cclin: read: 1191247896-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62E/cclin: read: 8-E/cclin: read: 1191247897-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62
复制代码

4.总结

通过日志分析,可以看到 pes 包的 stream_id 仅能找到 0xe0(224) ,无法找到音频的 stream_id(0xc0)且播放器对 pid=257 没有处理,而 257 刚好是我们媒体服务 ts 文件中的音频帧,所以,这就是 ExoPlayer 无法播放 ts 文件音频的原因,内部程序做了处理,这个是 bug 还是故意而为知,有待进一步分析。

发布于: 2 小时前阅读数: 5
用户头像

Changing Lin

关注

获得机遇的手段远超于固有常规之上~ 2020.04.29 加入

我能做的,就是调整好自己的精神状态,以最佳的面貌去面对那些未曾经历过得事情,对生活充满热情和希望。

评论

发布
暂无评论
ExoPlayer播放在线TS文件无声音问题分析