RtspSessionTiming.java

/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.media3.exoplayer.rtsp;

import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.exoplayer.rtsp.RtspMessageUtil.checkManifestExpression;

import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.ParserException;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Represent the timing (RTSP Normal Playback Time format) of an RTSP session.
 *
 * <p>Currently only NPT is supported. See RFC2326 Section 3.6 for detail of NPT.
 */
@UnstableApi
/* package */ final class RtspSessionTiming {
  /** The default session timing starting from 0.000 and indefinite length, effectively live. */
  public static final RtspSessionTiming DEFAULT =
      new RtspSessionTiming(/* startTimeMs= */ 0, /* stopTimeMs= */ C.TIME_UNSET);

  // We only support npt=xxx-[xxx], but not npt=-xxx. See RFC2326 Section 3.6.
  // Supports both npt= and npt: identifier.
  private static final Pattern NPT_RANGE_PATTERN =
      Pattern.compile("npt[:=]([.\d]+|now)\s?-\s?([.\d]+)?");
  private static final String START_TIMING_NTP_FORMAT = "npt=%.3f-";

  private static final long LIVE_START_TIME = 0;

  /** Parses an SDP range attribute (RFC2326 Section 3.6). */
  public static RtspSessionTiming parseTiming(String sdpRangeAttribute) throws ParserException {
    long startTimeMs;
    long stopTimeMs;
    Matcher matcher = NPT_RANGE_PATTERN.matcher(sdpRangeAttribute);
    checkManifestExpression(matcher.matches(), /* message= */ sdpRangeAttribute);

    @Nullable String startTimeString = matcher.group(1);
    checkManifestExpression(startTimeString != null, /* message= */ sdpRangeAttribute);
    if (castNonNull(startTimeString).equals("now")) {
      startTimeMs = LIVE_START_TIME;
    } else {
      startTimeMs = (long) (Float.parseFloat(startTimeString) * C.MILLIS_PER_SECOND);
    }

    @Nullable String stopTimeString = matcher.group(2);
    if (stopTimeString != null) {
      try {
        stopTimeMs = (long) (Float.parseFloat(stopTimeString) * C.MILLIS_PER_SECOND);
      } catch (NumberFormatException e) {
        throw ParserException.createForMalformedManifest(stopTimeString, e);
      }
      checkManifestExpression(stopTimeMs >= startTimeMs, /* message= */ sdpRangeAttribute);
    } else {
      stopTimeMs = C.TIME_UNSET;
    }

    return new RtspSessionTiming(startTimeMs, stopTimeMs);
  }

  /** Gets a Range RTSP header for an RTSP PLAY request. */
  public static String getOffsetStartTimeTiming(long offsetStartTimeMs) {
    double offsetStartTimeSec = (double) offsetStartTimeMs / C.MILLIS_PER_SECOND;
    return Util.formatInvariant(START_TIMING_NTP_FORMAT, offsetStartTimeSec);
  }

  /**
   * The start time of this session, in milliseconds. When playing a live session, the start time is
   * always zero.
   */
  public final long startTimeMs;
  /**
   * The stop time of the session, in milliseconds, or {@link C#TIME_UNSET} when the stop time is
   * not set, for example when playing a live session.
   */
  public final long stopTimeMs;

  private RtspSessionTiming(long startTimeMs, long stopTimeMs) {
    this.startTimeMs = startTimeMs;
    this.stopTimeMs = stopTimeMs;
  }

  /** Tests whether the timing is live. */
  public boolean isLive() {
    return stopTimeMs == C.TIME_UNSET;
  }

  /** Gets the session duration in milliseconds. */
  public long getDurationMs() {
    return stopTimeMs - startTimeMs;
  }
}