1.问题发现
在使用ExoPlayer过程中发现有些TS流获取的时长为-9223372036854775807 ,就是没有正确获取到时长,返回了默认值C.TIME_UNSET
Java
/**
* Special constant representing an unset or unknown time or duration. Suitable for use in any
* time base.
*/
public static final long TIME_UNSET = Long.MIN_VALUE + 1;
2.确定媒资信息
用ffmpeg命令查看媒资基本信息
sh
ffprobe -v quiet -print_format json -show_format "http://xxx.ts"
结果如下
json
{
"format": {
"filename": "http://xxx.ts",
"nb_streams": 2,
"nb_programs": 1,
"nb_stream_groups": 0,
"format_name": "mpegts",
"format_long_name": "MPEG-TS (MPEG-2 Transport Stream)",
"start_time": "1.400000",
"duration": "7117.674656",
"size": "1375406380",
"bit_rate": "25682818",
"probe_score": 50
}
}
基本信息
- 文件格式: MPEG-TS (MPEG-2 Transport Stream)
- 文件大小: 约 1.37 GB (1,375,406,380 字节)
- 总时长: 约 7117.67 秒
- 比特率: 约 25.68 Mbps (高码率流媒体)
可以看到有节目时长信息的,接下来简单分析下ExoPlayer是怎么处理TS媒资时长的
3.问题分析
Java
final class TsDurationReader {
/**
* Reads a TS duration from the input, using the given PCR PID.
*
* <p>This reader reads the duration by reading PCR values of the PCR PID packets at the start and
* at the end of the stream, calculating the difference, and converting that into stream duration.
*
* @param input The {@link ExtractorInput} from which data should be read.
* @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
* to hold the position of the required seek.
* @param pcrPid The PID of the packet stream within this TS stream that contains PCR values.
* @return One of the {@code RESULT_} values defined in {@link Extractor}.
* @throws IOException If an error occurred reading from the input.
*/
public @Extractor.ReadResult int readDuration(
ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) throws IOException {
//pcrPid有效性检查
if (pcrPid <= 0) {
return finishReadDuration(input);
}
if (!isLastPcrValueRead) {
//读取最后一个 PCR 值
Log.d("sssss","readDuration return 000");
return readLastPcrValue(input, seekPositionHolder, pcrPid);
}
if (lastPcrValue == C.TIME_UNSET) {
Log.d("sssss","readDuration return 111");
return finishReadDuration(input);
}
if (!isFirstPcrValueRead) {
//读取第一个 PCR 值
return readFirstPcrValue(input, seekPositionHolder, pcrPid);
}
if (firstPcrValue == C.TIME_UNSET) {
return finishReadDuration(input);
}
long minPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(firstPcrValue);
long maxPcrPositionUs =
pcrTimestampAdjuster.adjustTsTimestampGreaterThanPreviousTimestamp(lastPcrValue);
//计算差值得到持续时间
durationUs = maxPcrPositionUs - minPcrPositionUs;
return finishReadDuration(input);
}
}
readDuration方法是 TsDurationReader类的核心方法,用于从 MPEG-TS 流中读取持续时间。
PCR 全称: Program Clock Reference (节目时钟参考) , 用于同步解码器和编码器的时钟,确保音视频播放的正确时序 , 可以帮助计算媒体流的持续时间。
调试发现在readDuration return 000 的位置return,也就是lastPcrValue没有取到有效值,接着看lastPcrValue赋值的地方
Java
private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
throws IOException {
long inputLength = input.getLength();
//首先定位到倒数timestampSearchBytes处
int bytesToSearch = (int) min(timestampSearchBytes, inputLength);
long searchStartPosition = inputLength - bytesToSearch;
if (input.getPosition() != searchStartPosition) {
seekPositionHolder.position = searchStartPosition;
//若未达指定位置则发起SEEK
return Extractor.RESULT_SEEK;
}
packetBuffer.reset(bytesToSearch);
input.resetPeekPosition();
input.peekFully(packetBuffer.getData(), /* offset= */ 0, bytesToSearch);
//到达后将数据载入缓冲区,调用readLastPcrValueFromBuffer解析出最后一个PCR值并保存。
lastPcrValue = readLastPcrValueFromBuffer(packetBuffer, pcrPid);
isLastPcrValueRead = true;
return Extractor.RESULT_CONTINUE;
}
readLastPcrValueFromBuffer
Java
private long readLastPcrValueFromBuffer(ParsableByteArray packetBuffer, int pcrPid) {
int searchStartPosition = packetBuffer.getPosition();
int searchEndPosition = packetBuffer.limit();
// We start searching 'TsExtractor.TS_PACKET_SIZE' bytes from the end to prevent trying to read
// from an incomplete TS packet.
for (int searchPosition = searchEndPosition - TsExtractor.TS_PACKET_SIZE;
searchPosition >= searchStartPosition;
searchPosition--) {
if (!TsUtil.isStartOfTsPacket(
packetBuffer.getData(), searchStartPosition, searchEndPosition, searchPosition)) {
continue;
}
//从ts包从后往前检索PCR
long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid);
if (pcrValue != C.TIME_UNSET) {
return pcrValue;
}
}
return C.TIME_UNSET;
}
readPcrFromPacket
Java
/**
* Returns the PCR value read from a given TS packet.
*
* @param packetBuffer The buffer that holds the packet.
* @param startOfPacket The starting position of the packet in the buffer.
* @param pcrPid The PID for valid packets that contain PCR values.
* @return The PCR value read from the packet, if its PID is equal to {@code pcrPid} and it
* contains a valid PCR value. Returns {@link C#TIME_UNSET} otherwise.
*/
public static long readPcrFromPacket(
ParsableByteArray packetBuffer, int startOfPacket, int pcrPid) {
packetBuffer.setPosition(startOfPacket);
//...
boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0;
if (!adaptationFieldExists) {
Log.d("sssss","readDuration return aaa");
return C.TIME_UNSET;
}
Log.d("sssss","readDuration return bbb");
int adaptationFieldLength = packetBuffer.readUnsignedByte();
if (adaptationFieldLength >= 7 && packetBuffer.bytesLeft() >= 7) {
int flags = packetBuffer.readUnsignedByte();
boolean pcrFlagSet = (flags & 0x10) == 0x10;
if (pcrFlagSet) {
byte[] pcrBytes = new byte[6];
packetBuffer.readBytes(pcrBytes, /* offset= */ 0, pcrBytes.length);
Log.d("sssss","readDuration return ccc");
return readPcrValueFromPcrBytes(pcrBytes);
}
}
return C.TIME_UNSET;
}
调试发现在readDuration return aaa 的位置return
这段代码用于判断TS包头中的adaptation_field_control字段是否表示存在适配字段,而PCR存储在适配字段中,从而无法解析到媒资时长
4.问题处理
上述readLastPcrValue 方法中,用timestampSearchBytes设置的检索范围,往上追踪可以找到默认值:
Java
//每个ts包大小
public static final int TS_PACKET_SIZE = 188;
//默认600个ts包大小
public static final int DEFAULT_TIMESTAMP_SEARCH_BYTES = 600 * TS_PACKET_SIZE;
由于媒资是高码率流媒体,默认的 TS包 范围内 PCR 样本搜索范围不足,需要适当增加搜索范围以适应高码率流
Java
// MPEG-TS 流中搜索 PCR(Program Clock Reference) 时间戳时,需要扫描的字节数范围
DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
//默认600个ts包,适当增大
extractorsFactory.setTsExtractorTimestampSearchBytes(700 * 188);
DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(this,extractorsFactory);
player = new ExoPlayer.Builder(this)
.setMediaSourceFactory(mediaSourceFactory)
.build();