Media3 ExoPlayer获取不到TS流时长分析

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();
相关推荐
木西10 天前
短视频图文创作不求人:适合新手的工具推荐
音视频开发
哔哩哔哩技术13 天前
B站多模态精细画质分析模型在 ICCV2025 大赛获得佳绩
音视频开发
鹏多多16 天前
前端音频兼容解决:音频神器howler.js从基础到进阶完整使用指南
前端·javascript·音视频开发
百度Geek说1 个月前
百度电商MultiAgent视频生成系统
aigc·音视频开发
字节跳动视频云技术团队1 个月前
字节跳动多媒体实验室联合ISCAS举办第五届神经网络视频编码竞赛
人工智能·云计算·音视频开发
x007xyz1 个月前
🚀🚀🚀前端的无限可能-纯Web实现的字幕视频工具 FlyCut Caption
前端·openai·音视频开发