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@ee24d57
E/cclin: read: 0-
E/cclin: consume: programCount = 1
E/cclin: consume: xxxxxxxxx 4095-null
E/cclin: 构造PmtReader: 4095
E/cclin: read: 1196424976-1-4095-false-true====com.google.android.exoplayer2.extractor.ts.SectionReader@3235a044
E/cclin: read: 0-
E/cclin: 解析pmt的streamType: 27--1
E/cclin: 解析pmt的streamType: 6--1
E/cclin: read: 1195376656-1-0-false-true====null
E/cclin: read: 1196424976-1-4095-false-true====null
E/cclin: read: 1195442480-1-257-true-true====null
E/cclin: read: 1195376657-1-0-false-true====null
E/cclin: read: 1196424977-1-4095-false-true====null
E/cclin: read: 1195442481-1-257-true-true====null
E/cclin: read: 1195376658-1-0-false-true====null
E/cclin: read: 1196424978-1-4095-false-true====null
E/cclin: read: 1195442482-1-257-true-true====null
E/cclin: read: 1195442483-1-257-true-true====null
E/cclin: read: 1195442484-1-257-true-true====null
E/cclin: read: 1195442485-1-257-true-true====null
E/cclin: read: 1195442486-1-257-true-true====null
E/cclin: read: 1195442487-1-257-true-true====null
E/cclin: read: 1195442488-1-257-true-true====null
E/cclin: read: 1195442489-1-257-true-true====null
E/cclin: read: 1195442490-1-257-true-true====null
E/cclin: read: 1195442491-1-257-true-true====null
E/cclin: read: 1195376659-1-0-false-true====null
E/cclin: read: 1196424979-1-4095-false-true====null
E/cclin: read: 1195442492-1-257-true-true====null
E/cclin: read: 1195442493-1-257-true-true====null
E/cclin: read: 1195442494-1-257-true-true====null
E/cclin: read: 1195376660-1-0-false-true====null
E/cclin: read: 1196424980-1-4095-false-true====null
E/cclin: read: 1195442224-1-256-true-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62
E/cclin: read: 0-
E/cclin: 读取到 224
E/cclin: read: 1191247889-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62
E/cclin: read: 1-
E/cclin: read: 1191247890-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62
E/cclin: read: 2-
E/cclin: read: 1191247891-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62
E/cclin: read: 3-
E/cclin: read: 1191247892-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62
E/cclin: read: 4-
E/cclin: read: 1191247893-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62
E/cclin: read: 5-
E/cclin: read: 1191247894-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62
E/cclin: read: 6-
E/cclin: read: 1191247895-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62
E/cclin: read: 7-
E/cclin: read: 1191247896-0-256-false-true====com.google.android.exoplayer2.extractor.ts.PesReader@214bba62
E/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
版权声明: 本文为 InfoQ 作者【Changing Lin】的原创文章。
原文链接:【http://xie.infoq.cn/article/2d4303382ccfc1be17f143df7】。文章转载请联系作者。
Changing Lin
关注
获得机遇的手段远超于固有常规之上~ 2020.04.29 加入
我能做的,就是调整好自己的精神状态,以最佳的面貌去面对那些未曾经历过得事情,对生活充满热情和希望。
评论